s3: allow POST Object uploads when auth is off (#12273)

* s3: allow POST Object uploads when auth is off

With no identities configured every other handler skips authentication,
but PostPolicyBucketHandler still verified the form signature, so a
browser form upload failed with InvalidAccessKeyId (or a malformed
credential error without signature fields). Skip the signature check
when auth is off; the policy conditions are still enforced.

* s3: update AuthPostPolicy comment for the auth-off path

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
This commit is contained in:
Jay HemnaniandChris Lu authored and GitHub committed 2026-10-31 05:19:17 -07:00
1 parent 05584c541c
commit ecd716e0e2
3 files changed
+67 -14

No files matched your search

+3 -4
View File
@@ -1698,10 +1698,9 @@ func (iam *IdentityAccessManagement) Auth(f http.HandlerFunc, action Action) htt
// 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.
// because PostPolicy signature verification happens in PostPolicyBucketHandler when auth
// is enabled. This delegation only defers the initial authentication classification; it
// does NOT bypass signature verification for authenticated uploads.
func (iam *IdentityAccessManagement) AuthPostPolicy(f http.HandlerFunc, action Action) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
scrubInternalHeaders(r)
+14 -10
View File
@@ -80,16 +80,20 @@ func (s3a *S3ApiServer) PostPolicyBucketHandler(w http.ResponseWriter, r *http.R
}
}
// Verify policy signature.
identity, errCode := s3a.iam.doesPolicySignatureMatch(r.Context(), formValues)
if errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, refineDenialForBucketName(r, identity, errCode))
return
}
if identity != nil {
r = r.WithContext(recordIdentityInContext(r, identity))
if identity.Account != nil {
r.Header.Set(s3_constants.AmzAccountId, identity.Account.Id)
// Verify policy signature. With auth off there are no keys to check it against, as on the other handlers.
var identity *Identity
errCode := s3err.ErrNone
if s3a.iam.isEnabled() {
identity, errCode = s3a.iam.doesPolicySignatureMatch(r.Context(), formValues)
if errCode != s3err.ErrNone {
s3err.WriteErrorResponse(w, r, refineDenialForBucketName(r, identity, errCode))
return
}
if identity != nil {
r = r.WithContext(recordIdentityInContext(r, identity))
if identity.Account != nil {
r.Header.Set(s3_constants.AmzAccountId, identity.Account.Id)
}
}
}
@@ -683,3 +683,53 @@ func TestPostPolicyBucketHandler_PolicyViolationReturns403(t *testing.T) {
assert.NotContains(t, rec.Body.String(), "InvalidAccessKeyId",
"access key must be known so the handler reaches the policy-check branch")
}
// With auth off there are no keys, so a POST Object form must not be rejected
// for its signature; the policy conditions still apply.
func TestPostPolicyBucketHandler_AuthDisabledSkipsSignature(t *testing.T) {
const testBucket = "test-bucket"
iam := &IdentityAccessManagement{
hashes: make(map[string]*sync.Pool),
hashCounters: make(map[string]*int32),
}
assert.NoError(t, iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{}))
assert.False(t, iam.isEnabled())
s3a := &S3ApiServer{
option: &S3ApiServerOption{BucketsPath: "/buckets"},
iam: iam,
}
s3a.bucketRegistry = NewBucketRegistry(s3a)
s3a.bucketRegistry.setMetadataCache(&BucketMetaData{Name: testBucket, IsTableBucket: false})
now := time.Now().UTC()
credential := fmt.Sprintf("ANYKEY/%s/us-east-1/s3/aws4_request", now.Format(yyyymmdd))
policyJSON := fmt.Sprintf(`{"expiration":"%s","conditions":[["eq","$key","required.txt"]]}`,
now.Add(time.Hour).Format("2006-01-02T15:04:05.000Z"))
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
assert.NoError(t, writer.WriteField("key", "wrong.txt"))
assert.NoError(t, writer.WriteField("x-amz-credential", credential))
assert.NoError(t, writer.WriteField("x-amz-algorithm", "AWS4-HMAC-SHA256"))
assert.NoError(t, writer.WriteField("x-amz-date", now.Format(iso8601Format)))
assert.NoError(t, writer.WriteField("policy", base64.StdEncoding.EncodeToString([]byte(policyJSON))))
assert.NoError(t, writer.WriteField("x-amz-signature", "00"))
filePart, err := writer.CreateFormFile("file", "payload.txt")
assert.NoError(t, err)
_, err = filePart.Write([]byte("contents"))
assert.NoError(t, err)
assert.NoError(t, writer.Close())
req := httptest.NewRequest(http.MethodPost, "/"+testBucket, &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
req = mux.SetURLVars(req, map[string]string{"bucket": testBucket})
rec := httptest.NewRecorder()
s3a.PostPolicyBucketHandler(rec, req)
assert.NotContains(t, rec.Body.String(), "InvalidAccessKeyId")
assert.Contains(t, rec.Body.String(), "Policy Condition failed",
"the policy check must still run, body: %s", rec.Body.String())
}