mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-23 16:34:33 +00:00
* s3: count seaweedfs-quota as an operation subresource PUT /bucket?policy&seaweedfs-quota was not rejected by hasAmbiguousSubresource because operationSubresources omitted the seaweedfs-quota key. The router then picks the policy route (registered first) while the IAM action resolver may resolve the request to s3:PutBucketQuota, letting a quota-only identity write a bucket policy. Reject the combination before routing, matching the fix for policy&tagging (#10987). * s3: resolve seaweedfs-quota after other bucket subresources The quota routes are registered last among the bucket subresource routes, but the action resolver found seaweedfs-quota inside the unordered bucketQueryActions map, so a request carrying it alongside another selector could be authorized as the quota operation while the router served the earlier-registered handler. Resolve it explicitly at the end so the resolver agrees with the router, mirroring how list-type is handled. * s3: count resolver subresources in the ambiguity guard hasAmbiguousSubresource only counted operationSubresources, so adding a query parameter to the action resolver without updating that list reopened the authorize-one-serve-another gap. Count bucketQueryActions keys as operation selectors too, and add a test that walks the registered routes and fails on any query key that is neither an operation subresource nor a known modifier.
128 lines
4.3 KiB
Go
128 lines
4.3 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, "seaweedfs-quota": 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 {
|
|
// bucketQueryActions keys select an operation by definition, so they
|
|
// count even if operationSubresources was not updated for them.
|
|
if _, ok := bucketQueryActions[key]; !ok && !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)
|
|
})
|
|
}
|