Browse Source

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
pull/7919/head
Chris Lu 1 day ago
committed by GitHub
parent
commit
7a18c3a16f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 124
      weed/s3api/auth_credentials.go
  2. 8
      weed/s3api/s3api_auth.go
  3. 2
      weed/s3api/s3api_server.go

124
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

8
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 {

2
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) {

Loading…
Cancel
Save