fix: adds error routes to reject x-amz-copy-source for GET, POST, HEAD, DELETErequests

Fixes #1612

`x-amz-copy-source` is rejected with an **InvalidArgument** error in S3 for all HTTP methods other than **PUT** (i.e., **GET**, **POST**, **HEAD**, and **DELETE**). For **POST** requests, the behavior is slightly different: the error is returned only when the **uploadId** query parameter is present; otherwise, **MethodNotAllowed** is returned. This behavior applies to both bucket-level and object-level operations.
This commit is contained in:
niksis02
2025-11-13 20:49:40 +04:00
parent 9f54a25519
commit 4740372ce2
4 changed files with 115 additions and 0 deletions

View File

@@ -89,6 +89,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
}
// ListBuckets action
// copy source is not allowed on '/'
app.Get("/", middlewares.MatchHeader("X-Amz-Copy-Source"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
app.Get("/",
controllers.ProcessHandlers(
ctrl.ListBuckets,
@@ -384,6 +390,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
))
// HeadBucket action
// copy source is not allowed on bucket HEAD operation
bucketRouter.Head("/", middlewares.MatchHeader("X-Amz-Copy-Source"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
bucketRouter.Head("",
controllers.ProcessHandlers(
ctrl.HeadBucket,
@@ -399,6 +411,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
))
// DELETE bucket operations
// copy source is not allowed on bucket DELETE operation
bucketRouter.Delete("/", middlewares.MatchHeader("X-Amz-Copy-Source"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
bucketRouter.Delete("",
middlewares.MatchQueryArgs("tagging"),
controllers.ProcessHandlers(
@@ -582,6 +600,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
))
// GET bucket operations
// copy source is not allowed on bucket GET operation
bucketRouter.Get("/", middlewares.MatchHeader("X-Amz-Copy-Source"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
bucketRouter.Get("",
middlewares.MatchQueryArgs("location"),
controllers.ProcessHandlers(
@@ -973,6 +997,13 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
middlewares.ParseAcl(be),
))
// bucket POST operation is not allowed with uploadId and copy source
bucketRouter.Post("/",
middlewares.MatchHeader("X-Amz-Copy-Source"),
middlewares.MatchQueryArgs("uploadId"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
// DeleteObjects action
bucketRouter.Post("",
middlewares.MatchQueryArgs("delete"),
@@ -989,6 +1020,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
middlewares.ParseAcl(be),
))
// object HEAD operation is not allowed with copy source
objectRouter.Head("/",
middlewares.MatchHeader("X-Amz-Copy-Source"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
// HeadObject
objectRouter.Head("",
controllers.ProcessHandlers(
@@ -1011,6 +1048,12 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrGetUploadsWithKey)), metrics.ActionUndetected, services),
)
// object GET operation is not allowed with copy source
objectRouter.Get("/",
middlewares.MatchHeader("X-Amz-Copy-Source"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
objectRouter.Get("",
middlewares.MatchQueryArgs("tagging"),
controllers.ProcessHandlers(
@@ -1103,6 +1146,13 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
))
// DELETE object operations
// object DELETE operation is not allowed with copy source
objectRouter.Delete("/",
middlewares.MatchHeader("X-Amz-Copy-Source"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
objectRouter.Delete("",
middlewares.MatchQueryArgs("tagging"),
controllers.ProcessHandlers(
@@ -1142,6 +1192,15 @@ func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMServ
middlewares.ParseAcl(be),
))
// object POST operations
// object POST operation is not allowed with copy source and uploadId
objectRouter.Post("/",
middlewares.MatchHeader("X-Amz-Copy-Source"),
middlewares.MatchQueryArgs("uploadId"),
controllers.ProcessHandlers(ctrl.HandleErrorRoute(s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)), metrics.ActionUndetected, services),
)
objectRouter.Post("",
middlewares.MatchQueryArgs("restore"),
controllers.ProcessHandlers(

View File

@@ -124,6 +124,7 @@ const (
ErrRequestNotReadyYet
ErrMissingDateHeader
ErrGetUploadsWithKey
ErrCopySourceNotAllowed
ErrInvalidRequest
ErrAuthNotSetup
ErrNotImplemented
@@ -521,6 +522,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Key is not expected for the GET method ?uploads subresource",
HTTPStatusCode: http.StatusBadRequest,
},
ErrCopySourceNotAllowed: {
Code: "InvalidArgument",
Description: "You can only specify a copy source header for copy requests.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidRequest: {
Code: "InvalidRequest",
Description: "Invalid Request.",

View File

@@ -15,6 +15,7 @@
package integration
import (
"fmt"
"net/http"
"time"
@@ -141,6 +142,53 @@ func RouterGetUploadsWithKey(s *S3Conf) error {
})
}
func RouterCopySourceNotAllowed(s *S3Conf) error {
testName := "RouterCopySourceNotAllowed"
return actionHandlerNoSetup(s, testName, func(s3client *s3.Client, bucket string) error {
for _, method := range []string{
http.MethodPost,
http.MethodDelete,
http.MethodGet,
http.MethodHead,
} {
for _, path := range []string{
"/bucket",
"/bucket/object",
} {
if method == http.MethodPost {
// the error for POST request occurs only when uploadId is there
path += "?uploadId=something"
}
req, err := http.NewRequest(method, s.endpoint+path, nil)
if err != nil {
return fmt.Errorf("failed to make %s request to %s", method, path)
}
req.Header.Add("x-amz-copy-source", "bucket/object")
resp, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send %s request to %s", method, path)
}
if method == http.MethodHead {
// for head requests only check the status code
if resp.StatusCode != http.StatusBadRequest {
return fmt.Errorf("expected 400 status code for HEAD %s request, instead got %v", path, resp.StatusCode)
}
} else {
if err := checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrCopySourceNotAllowed)); err != nil {
return fmt.Errorf("%s %s: %w", method, path, err)
}
}
}
}
return nil
})
}
// CORS middleware tests
func CORSMiddleware_invalid_method(s *S3Conf) error {
testName := "CORSMiddleware_invalid_method"

View File

@@ -1087,6 +1087,7 @@ func TestRouter(ts *TestState) {
ts.Run(RouterPostObjectWithoutQuery)
ts.Run(RouterPUTObjectOnlyUploadId)
ts.Run(RouterGetUploadsWithKey)
ts.Run(RouterCopySourceNotAllowed)
}
type IntTest func(s3 *S3Conf) error
@@ -1724,5 +1725,6 @@ func GetIntTests() IntTests {
"RouterPostObjectWithoutQuery": RouterPostObjectWithoutQuery,
"RouterPUTObjectOnlyUploadId": RouterPUTObjectOnlyUploadId,
"RouterGetUploadsWithKey": RouterGetUploadsWithKey,
"RouterCopySourceNotAllowed": RouterCopySourceNotAllowed,
}
}