fix: reject an empty start-after and continuation-token in ListObjects (V1)

This commit is contained in:
dualfroz
2026-10-12 14:30:16 +02:00
parent 44dc2e71ec
commit aed4617372
4 changed files with 83 additions and 16 deletions
+11 -2
View File
@@ -615,8 +615,17 @@ func (c S3ApiController) ListObjects(ctx fiber.Ctx) (*Response, error) {
region = defaultRegion
}
// start-after is a ListObjectsV2 argument, S3 rejects it on a V1 listing
if ctx.Query("start-after") != "" {
// continuation-token and start-after are ListObjectsV2 arguments, S3
// rejects either one on a V1 listing, with or without a value
queryArgs := ctx.Request().URI().QueryArgs()
if queryArgs.Has("continuation-token") {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetInvalidArgumentErr(s3err.InvalidArgContinuationToken, "")
}
if queryArgs.Has("start-after") {
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
+36
View File
@@ -1213,6 +1213,42 @@ func TestS3ApiController_ListObjects(t *testing.T) {
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, ""),
},
},
{
name: "empty start-after is not supported",
input: testInput{
locals: defaultLocals,
beRes: listResult,
queries: map[string]string{
"start-after": "",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, ""),
},
},
{
name: "continuation-token is not supported",
input: testInput{
locals: defaultLocals,
beRes: listResult,
queries: map[string]string{
"continuation-token": "my-token",
},
},
output: testOutput{
response: &Response{
MetaOpts: &MetaOptions{
BucketOwner: "root",
},
},
err: s3err.GetInvalidArgumentErr(s3err.InvalidArgContinuationToken, ""),
},
},
{
name: "invalid max keys",
input: testInput{
+5
View File
@@ -27,6 +27,7 @@ const (
InvalidArgMaxBuckets InvalidArgErrorCode = iota
InvalidArgNegativeMaxKeys
InvalidArgStartAfter
InvalidArgContinuationToken
InvalidArgObjectAttributes
InvalidArgPartNumber
InvalidArgCompleteMpPartNumber
@@ -95,6 +96,10 @@ var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{
Description: "startAfter only supported in REST.GET.BUCKET with list-type=2",
ArgumentName: "start-after",
},
InvalidArgContinuationToken: {
Description: "continuation-token only supported in REST.GET.BUCKET with list-type=2",
ArgumentName: "continuation-token",
},
InvalidArgObjectAttributes: {
Description: "Invalid attribute name specified.",
ArgumentName: "x-amz-object-attributes",
+31 -14
View File
@@ -195,9 +195,9 @@ func ListObjects_invalid_max_keys(s *S3Conf) error {
})
}
// start-after belongs to ListObjectsV2, so S3 rejects it on a V1 listing
// instead of ignoring it. The SDK ListObjects input has no StartAfter field,
// so the request is signed and sent by hand.
// start-after and continuation-token belong to ListObjectsV2, so S3 rejects
// either one on a V1 listing, with or without a value. The SDK ListObjects
// input has neither field, so the requests are signed and sent by hand.
func ListObjects_start_after(s *S3Conf) error {
testName := "ListObjects_start_after"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
@@ -206,19 +206,36 @@ func ListObjects_start_after(s *S3Conf) error {
return err
}
req, err := createSignedReq(http.MethodGet, s.endpoint,
fmt.Sprintf("%v?start-after=bar", bucket),
s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), nil)
if err != nil {
return err
startAfterErr := s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, "")
contTokenErr := s3err.GetInvalidArgumentErr(s3err.InvalidArgContinuationToken, "")
for _, test := range []struct {
query string
expected s3err.S3Error
}{
{"start-after=bar", startAfterErr},
{"start-after=", startAfterErr},
{"start-after", startAfterErr},
{"continuation-token=bar", contTokenErr},
{"continuation-token=", contTokenErr},
} {
req, err := createSignedReq(http.MethodGet, s.endpoint,
fmt.Sprintf("%v?%v", bucket, test.query),
s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), nil)
if err != nil {
return err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
if err := checkHTTPResponseApiErr(resp, test.expected); err != nil {
return fmt.Errorf("%v: %w", test.query, err)
}
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
return checkHTTPResponseApiErr(resp, s3err.GetInvalidArgumentErr(s3err.InvalidArgStartAfter, ""))
return nil
})
}