From eecc1a779c2069db756e5d8549c51add9beab2fd Mon Sep 17 00:00:00 2001 From: niksis02 Date: Wed, 20 May 2026 20:08:43 +0400 Subject: [PATCH] fix: reject invalid PostObject keys Validate multipart PostObject key fields with the existing object name rules so path traversal and degenerate names return BadRequest. This prevents crafted object keys from escaping the gateway root. --- s3api/controllers/bucket-post.go | 10 +------ s3api/controllers/bucket-post_test.go | 22 ---------------- s3api/middlewares/object-post-auth.go | 10 +++++++ tests/integration/PostObject.go | 38 +++++++++++++++++++++++++++ tests/integration/group-tests.go | 2 ++ 5 files changed, 51 insertions(+), 31 deletions(-) diff --git a/s3api/controllers/bucket-post.go b/s3api/controllers/bucket-post.go index e22c420c..1fd0a825 100644 --- a/s3api/controllers/bucket-post.go +++ b/s3api/controllers/bucket-post.go @@ -112,15 +112,7 @@ func (c S3ApiController) POSTObject(ctx *fiber.Ctx) (*Response, error) { cacheControl := parsed.Fields["cache-control"] expires := parsed.Fields["expires"] - key, ok := parsed.Fields["key"] - if !ok || key == "" { - debuglogger.Logf("missing object key") - return &Response{ - MetaOpts: &MetaOptions{ - BucketOwner: parsedAcl.Owner, - }, - }, s3err.PostAuth.MissingField("key") - } + key := parsed.Fields["key"] err := auth.VerifyAccess(ctx.Context(), c.be, auth.AccessOptions{ diff --git a/s3api/controllers/bucket-post_test.go b/s3api/controllers/bucket-post_test.go index 0788c04f..4d4aa968 100644 --- a/s3api/controllers/bucket-post_test.go +++ b/s3api/controllers/bucket-post_test.go @@ -254,28 +254,6 @@ func TestS3ApiController_POSTObject(t *testing.T) { input testInput output testOutput }{ - { - name: "missing key", - input: testInput{ - locals: postObjectLocalsForTest(middlewares.PostObjectResult{ - Fields: map[string]string{ - "policy": basePolicy, - "file": "ignored", - "x-amz-signature": "ignored", - }, - FileRdr: newMockFileReader("payload"), - ContentLength: int64(len("payload")), - }), - }, - output: testOutput{ - response: &Response{ - MetaOpts: &MetaOptions{ - BucketOwner: "root", - }, - }, - err: s3err.PostAuth.MissingField("key"), - }, - }, { name: "verify access fails", input: testInput{ diff --git a/s3api/middlewares/object-post-auth.go b/s3api/middlewares/object-post-auth.go index 10cabdef..7567c885 100644 --- a/s3api/middlewares/object-post-auth.go +++ b/s3api/middlewares/object-post-auth.go @@ -86,6 +86,16 @@ func AuthorizePostObject(root RootUserConfig, iam auth.IAMService, region string fields := result.Fields + if fields["key"] == "" { + debuglogger.Logf("missing object key") + return s3err.PostAuth.MissingField("key") + } + + if !utils.IsObjectNameValid(fields["key"]) { + debuglogger.Logf("invalid POST object key: %q", fields["key"]) + return s3err.GetAPIError(s3err.ErrBadRequest) + } + policyB64 := fields[formFieldPolicy] algorithm := fields[formFieldAlgorithm] credentialStr := fields[formFieldCredential] diff --git a/tests/integration/PostObject.go b/tests/integration/PostObject.go index 962a2443..d020ed6b 100644 --- a/tests/integration/PostObject.go +++ b/tests/integration/PostObject.go @@ -309,6 +309,44 @@ func PostObject_access_denied(s *S3Conf) error { }) } +func PostObject_invalid_object_names(s *S3Conf) error { + testName := "PostObject_invalid_object_names" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + for _, obj := range []string{ + ".", + "..", + "./", + "/.", + "//", + "../", + "/..", + "../.", + "../../../.", + "../../../etc/passwd", + "../../../../tmp/foo", + "for/../../bar/", + "a/a/a/../../../../../etc/passwd", + "/a/../../b/../../c/../../../etc/passwd", + } { + resp, err := sendPostObject(PostRequestConfig{ + bucket: bucket, + key: obj, + s3Conf: s, + fileContent: []byte("data"), + }) + if err != nil { + return err + } + + if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrBadRequest)); err != nil { + return err + } + } + + return nil + }) +} + func PostObject_policy_access_control(s *S3Conf) error { testName := "PostObject_policy_access_control" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 24c29d44..0bd7254b 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -1196,6 +1196,7 @@ func TestPostObject(ts *TestState) { ts.Run(PostObject_signature_mismatch) ts.Run(PostObject_expired_due_to_date) ts.Run(PostObject_access_denied) + ts.Run(PostObject_invalid_object_names) ts.Run(PostObject_policy_access_control) ts.Run(PostObject_policy_expired) ts.Run(PostObject_invalid_policy_document) @@ -2038,6 +2039,7 @@ func GetIntTests() IntTests { "PostObject_signature_mismatch": PostObject_signature_mismatch, "PostObject_expired_due_to_date": PostObject_expired_due_to_date, "PostObject_access_denied": PostObject_access_denied, + "PostObject_invalid_object_names": PostObject_invalid_object_names, "PostObject_policy_access_control": PostObject_policy_access_control, "PostObject_policy_expired": PostObject_policy_expired, "PostObject_invalid_policy_document": PostObject_invalid_policy_document,