mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-14 10:30:45 +02:00
* s3: reject list-type paired with another operation subresource ?list-type=2&ownershipControls= routes to ListObjectsV2 (the list-type route is registered first) while the IAM action resolver resolves the ownershipControls selector to s3:GetBucketOwnershipControls. A principal denied s3:ListBucket but allowed s3:GetBucketOwnershipControls would therefore list the bucket. list-type selects an operation just like the other keys in operationSubresources, so add it there and reject the combination before routing, matching the fix for policy&tagging (#10987). * s3: resolve list-type to s3:ListBucket ahead of bucket subresources The router registers the ListObjectsV2 route ahead of the bucket subresource routes, so the action resolver should resolve list-type the same way. Without this, a request carrying list-type and another operation selector resolves to the subresource action (e.g. s3:GetBucketOwnershipControls) while being served by ListObjectsV2. The ambiguity guard rejects such combinations before routing, but resolving list-type to s3:ListBucket keeps the resolver aligned with the router, mirroring how versions is handled. * s3: match list-type=2 exactly in action resolver The router selects ListObjectsV2 only for list-type=2; other values fall through to the subresource routes. Resolve the same way so the action matches the handler for every list-type value, not just 2.
125 lines
4.1 KiB
Go
125 lines
4.1 KiB
Go
package s3api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
func hasPathSegmentQuery(rawQuery string) bool {
|
|
if strings.Contains(rawQuery, "versionId") || strings.Contains(rawQuery, "uploadId") {
|
|
return true
|
|
}
|
|
if !strings.Contains(rawQuery, "%") {
|
|
return false
|
|
}
|
|
|
|
for rawQuery != "" {
|
|
field := rawQuery
|
|
if i := strings.IndexByte(rawQuery, '&'); i >= 0 {
|
|
field, rawQuery = rawQuery[:i], rawQuery[i+1:]
|
|
} else {
|
|
rawQuery = ""
|
|
}
|
|
if i := strings.IndexByte(field, '='); i >= 0 {
|
|
field = field[:i]
|
|
}
|
|
if !strings.Contains(field, "%") {
|
|
continue
|
|
}
|
|
key, err := url.QueryUnescape(field)
|
|
if err == nil && (key == "versionId" || key == "uploadId") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// operationSubresources are the query keys that select which operation a request
|
|
// is, so at most one may appear. The router matches them in registration order
|
|
// and the IAM action resolver in its own, so a request carrying two is
|
|
// authorized as one operation and served as another: `PUT /bucket?policy&tagging`
|
|
// authorizes as PutBucketTagging and runs PutBucketPolicy. Keys left out here
|
|
// (versionId, partNumber, prefix, ...) modify an operation instead of selecting
|
|
// one and may accompany any of these.
|
|
var operationSubresources = map[string]bool{
|
|
"accelerate": true, "acl": true, "analytics": true, "attributes": true,
|
|
"cors": true, "delete": true, "encryption": true, "intelligent-tiering": true,
|
|
"inventory": true, "legal-hold": true, "lifecycle": true, "list-type": true,
|
|
"location": true, "logging": true, "metrics": true, "notification": true,
|
|
"object-lock": true, "ownershipControls": true, "policy": true, "policyStatus": true,
|
|
"publicAccessBlock": true, "renameObject": true, "replication": true,
|
|
"requestPayment": true, "retention": true, "tagging": true, "uploadId": true,
|
|
"uploads": true, "versioning": true, "versions": true, "website": true,
|
|
}
|
|
|
|
func hasAmbiguousSubresource(query url.Values) bool {
|
|
seen := 0
|
|
for key := range query {
|
|
if !operationSubresources[key] {
|
|
continue
|
|
}
|
|
if seen++; seen > 1 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasInvalidPathSegment(values []string) bool {
|
|
for _, value := range values {
|
|
if value != "" && !s3_constants.IsValidPathSegment(value) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// validateRequestPath rejects requests whose captured {bucket}/{object} mux
|
|
// vars would normalize to a parent-directory traversal once joined into a
|
|
// filer path. The router runs with mux.NewRouter().SkipClean(true), so
|
|
// segments like `..` survive routing; the filer's util.JoinPath later collapses
|
|
// them via filepath.Join. Without this guard, `GET /bucket-A/../evil-bucket/k`
|
|
// matches as bucket=bucket-A, object=../evil-bucket/k, the filer resolves the
|
|
// read against evil-bucket, while IAM authorizes against bucket-A.
|
|
func validateRequestPath(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
// When a var is in the matched route it must be non-empty: an empty
|
|
// bucket would let downstream path.Join collapse it and let the object
|
|
// key pick the bucket.
|
|
if bucket, ok := vars["bucket"]; ok {
|
|
if bucket == "" || !s3_constants.IsValidBucketName(bucket) {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
|
return
|
|
}
|
|
}
|
|
if object, ok := vars["object"]; ok {
|
|
if object == "" || !s3_constants.IsValidObjectKey(object) {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
|
return
|
|
}
|
|
}
|
|
if r.URL.RawQuery != "" {
|
|
query := r.URL.Query()
|
|
if hasAmbiguousSubresource(query) {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
|
return
|
|
}
|
|
// versionId and uploadId are later used as filer entry names, and
|
|
// the encoded spelling of either still names one.
|
|
if hasPathSegmentQuery(r.URL.RawQuery) {
|
|
if hasInvalidPathSegment(query["versionId"]) || hasInvalidPathSegment(query["uploadId"]) {
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|