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.
This commit is contained in:
niksis02
2026-05-20 20:08:43 +04:00
parent 81c9d4ed2f
commit eecc1a779c
5 changed files with 51 additions and 31 deletions

View File

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

View File

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

View File

@@ -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]

View File

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

View File

@@ -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,