From 7a18c3a16fea98ced731073b255143667d3f0dec Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 30 Dec 2025 12:40:59 -0800 Subject: [PATCH] Fix critical authentication bypass vulnerability (#7912) (#7915) * Fix critical authentication bypass vulnerability (#7912) The isRequestPostPolicySignatureV4() function was incorrectly returning true for ANY POST request with multipart/form-data content type, causing all such requests to bypass authentication in authRequest(). This allowed unauthenticated access to S3 API endpoints, as reported in issue #7912 where any credentials (or no credentials) were accepted. The fix removes isRequestPostPolicySignatureV4() entirely, preventing authTypePostPolicy from ever being set. PostPolicy signature verification is still properly handled in PostPolicyBucketHandler via doesPolicySignatureMatch(). Fixes #7912 * add AuthPostPolicy * refactor * Optimizing Auth Credentials * Update auth_credentials.go * Update auth_credentials.go --- weed/s3api/auth_credentials.go | 124 ++++++++++++++++++++++----------- weed/s3api/s3api_auth.go | 8 --- weed/s3api/s3api_server.go | 2 +- 3 files changed, 85 insertions(+), 49 deletions(-) diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 49f2acf87..0cbed72a2 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -529,73 +529,120 @@ func (iam *IdentityAccessManagement) Auth(f http.HandlerFunc, action Action) htt identity, errCode := iam.authRequest(r, action) glog.V(3).Infof("auth error: %v", errCode) - if errCode == s3err.ErrNone { - // Store the authenticated identity in request context (secure, cannot be spoofed) - if identity != nil && identity.Name != "" { - ctx := s3_constants.SetIdentityNameInContext(r.Context(), identity.Name) - // Also store the full identity object for handlers that need it (e.g., ListBuckets) - // This is especially important for JWT users whose identity is not in the identities list - ctx = s3_constants.SetIdentityInContext(ctx, identity) - r = r.WithContext(ctx) - } + iam.handleAuthResult(w, r, identity, errCode, f) + } +} + +// AuthPostPolicy is a specialized authentication wrapper for PostPolicy requests. +// It allows requests with multipart/form-data to proceed even if classified as Anonymous, +// because the actual authentication (signature verification) for ALL PostPolicy requests is +// performed unconditionally in PostPolicyBucketHandler.doesPolicySignatureMatch(). +// This delegation only defers the initial authentication classification; it does NOT bypass +// signature verification, which is mandatory for all PostPolicy uploads. +func (iam *IdentityAccessManagement) AuthPostPolicy(f http.HandlerFunc, action Action) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !iam.isEnabled() { f(w, r) return } - s3err.WriteErrorResponse(w, r, errCode) + + // Optimization: Use authRequestWithAuthType to avoid re-parsing headers for classification + identity, errCode, authType := iam.authRequestWithAuthType(r, action) + + // Special handling for PostPolicy: if AccessDenied (likely because Anonymous to private bucket) + // AND it looks like a PostPolicy request, allow it to proceed to handler for verification. + if errCode == s3err.ErrAccessDenied { + if authType == authTypeAnonymous && + r.Method == http.MethodPost && + strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") { + + glog.V(3).Infof("Delegating PostPolicy auth to handler") + r.Header.Set(s3_constants.AmzAuthType, "PostPolicy") + f(w, r) + return + } + } + + glog.V(3).Infof("auth error: %v", errCode) + + iam.handleAuthResult(w, r, identity, errCode, f) } } -// check whether the request has valid access keys +func (iam *IdentityAccessManagement) handleAuthResult(w http.ResponseWriter, r *http.Request, identity *Identity, errCode s3err.ErrorCode, f http.HandlerFunc) { + if errCode == s3err.ErrNone { + // Store the authenticated identity in request context (secure, cannot be spoofed) + if identity != nil && identity.Name != "" { + ctx := s3_constants.SetIdentityNameInContext(r.Context(), identity.Name) + // Also store the full identity object for handlers that need it (e.g., ListBuckets) + // This is especially important for JWT users whose identity is not in the identities list + ctx = s3_constants.SetIdentityInContext(ctx, identity) + r = r.WithContext(ctx) + } + f(w, r) + return + } + s3err.WriteErrorResponse(w, r, errCode) +} + +// Wrapper to maintain backward compatibility func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) (*Identity, s3err.ErrorCode) { + identity, err, _ := iam.authRequestWithAuthType(r, action) + return identity, err +} + +// check whether the request has valid access keys +func (iam *IdentityAccessManagement) authRequestWithAuthType(r *http.Request, action Action) (*Identity, s3err.ErrorCode, authType) { var identity *Identity var s3Err s3err.ErrorCode var found bool - var authType string - switch getRequestAuthType(r) { + var amzAuthType string + + reqAuthType := getRequestAuthType(r) + + switch reqAuthType { case authTypeUnknown: glog.V(3).Infof("unknown auth type") r.Header.Set(s3_constants.AmzAuthType, "Unknown") - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType case authTypePresignedV2, authTypeSignedV2: glog.V(3).Infof("v2 auth type") identity, s3Err = iam.isReqAuthenticatedV2(r) - authType = "SigV2" + amzAuthType = "SigV2" case authTypeStreamingSigned, authTypeSigned, authTypePresigned: glog.V(3).Infof("v4 auth type") identity, s3Err = iam.reqSignatureV4Verify(r) - authType = "SigV4" - case authTypePostPolicy: - glog.V(3).Infof("post policy auth type") - r.Header.Set(s3_constants.AmzAuthType, "PostPolicy") - return identity, s3err.ErrNone + amzAuthType = "SigV4" case authTypeStreamingUnsigned: glog.V(3).Infof("unsigned streaming upload") - return identity, s3err.ErrNone + // no amzAuthType set for this case in original code? + // Actually original explicitly returned ErrNone without setting identity + return identity, s3err.ErrNone, reqAuthType case authTypeJWT: glog.V(3).Infof("jwt auth type detected, iamIntegration != nil? %t", iam.iamIntegration != nil) r.Header.Set(s3_constants.AmzAuthType, "Jwt") if iam.iamIntegration != nil { identity, s3Err = iam.authenticateJWTWithIAM(r) - authType = "Jwt" + amzAuthType = "Jwt" } else { glog.V(2).Infof("IAM integration is nil, returning ErrNotImplemented") - return identity, s3err.ErrNotImplemented + return identity, s3err.ErrNotImplemented, reqAuthType } case authTypeAnonymous: - authType = "Anonymous" + amzAuthType = "Anonymous" if identity, found = iam.lookupAnonymous(); !found { - r.Header.Set(s3_constants.AmzAuthType, authType) - return identity, s3err.ErrAccessDenied + r.Header.Set(s3_constants.AmzAuthType, amzAuthType) + return identity, s3err.ErrAccessDenied, reqAuthType } default: - return identity, s3err.ErrNotImplemented + return identity, s3err.ErrNotImplemented, reqAuthType } - if len(authType) > 0 { - r.Header.Set(s3_constants.AmzAuthType, authType) + if len(amzAuthType) > 0 { + r.Header.Set(s3_constants.AmzAuthType, amzAuthType) } if s3Err != s3err.ErrNone { - return identity, s3Err + return identity, s3Err, reqAuthType } glog.V(3).Infof("user name: %v actions: %v, action: %v", identity.Name, identity.Actions, action) @@ -636,7 +683,7 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) // SECURITY: Fail-close on policy evaluation errors // If we can't evaluate the policy, deny access rather than falling through to IAM glog.Errorf("Error evaluating bucket policy for %s/%s: %v - denying access", bucket, object, err) - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType } else if evaluated { // A bucket policy exists and was evaluated with a matching statement if allowed { @@ -648,7 +695,7 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) // Policy explicitly denies this action - deny access immediately // Note: Explicit Deny in bucket policy overrides all other permissions glog.V(3).Infof("Bucket policy explicitly denies %s to %s on %s/%s", identity.Name, action, bucket, object) - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType } } // If not evaluated (no policy or no matching statements), fall through to IAM/identity checks @@ -660,21 +707,21 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) // JWT/STS identities (no Actions) use IAM authorization if len(identity.Actions) > 0 { if !identity.canDo(action, bucket, object) { - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType } } else if iam.iamIntegration != nil { if errCode := iam.authorizeWithIAM(r, identity, action, bucket, object); errCode != s3err.ErrNone { - return identity, errCode + return identity, errCode, reqAuthType } } else { - return identity, s3err.ErrAccessDenied + return identity, s3err.ErrAccessDenied, reqAuthType } } } r.Header.Set(s3_constants.AmzAccountId, identity.Account.Id) - return identity, s3err.ErrNone + return identity, s3err.ErrNone, reqAuthType } @@ -699,10 +746,7 @@ func (iam *IdentityAccessManagement) AuthSignatureOnly(r *http.Request) (*Identi glog.V(3).Infof("v4 auth type") identity, s3Err = iam.reqSignatureV4Verify(r) authType = "SigV4" - case authTypePostPolicy: - glog.V(3).Infof("post policy auth type") - r.Header.Set(s3_constants.AmzAuthType, "PostPolicy") - return identity, s3err.ErrNone + case authTypeStreamingUnsigned: glog.V(3).Infof("unsigned streaming upload") return identity, s3err.ErrNone diff --git a/weed/s3api/s3api_auth.go b/weed/s3api/s3api_auth.go index 5592fe939..6963373cd 100644 --- a/weed/s3api/s3api_auth.go +++ b/weed/s3api/s3api_auth.go @@ -41,12 +41,6 @@ func isRequestPresignedSignatureV2(r *http.Request) bool { return ok } -// Verify if request has AWS Post policy Signature Version '4'. -func isRequestPostPolicySignatureV4(r *http.Request) bool { - return strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") && - r.Method == http.MethodPost -} - // Verify if the request has AWS Streaming Signature Version '4'. This is only valid for 'PUT' operation. // Supports both with and without trailer variants: // - STREAMING-AWS4-HMAC-SHA256-PAYLOAD (original) @@ -101,8 +95,6 @@ func getRequestAuthType(r *http.Request) authType { authType = authTypePresigned } else if isRequestJWT(r) { authType = authTypeJWT - } else if isRequestPostPolicySignatureV4(r) { - authType = authTypePostPolicy } else if _, ok := r.Header["Authorization"]; !ok { authType = authTypeAnonymous } else { diff --git a/weed/s3api/s3api_server.go b/weed/s3api/s3api_server.go index c811d668b..5917b5195 100644 --- a/weed/s3api/s3api_server.go +++ b/weed/s3api/s3api_server.go @@ -573,7 +573,7 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) { // raw buckets // PostPolicy - bucket.Methods(http.MethodPost).HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(track(s3a.iam.Auth(s3a.cb.Limit(s3a.PostPolicyBucketHandler, ACTION_WRITE)), "POST")) + bucket.Methods(http.MethodPost).HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(track(s3a.iam.AuthPostPolicy(s3a.cb.Limit(s3a.PostPolicyBucketHandler, ACTION_WRITE)), "POST")) // HeadBucket bucket.Methods(http.MethodHead).HandlerFunc(track(s3a.AuthWithPublicRead(func(w http.ResponseWriter, r *http.Request) {