From 69e107efe9e8190674a979f529ceda24ec5391ef Mon Sep 17 00:00:00 2001 From: niksis02 Date: Mon, 8 Dec 2025 19:35:27 +0400 Subject: [PATCH] fix: rejects STREAMING-UNSIGNED-PAYLOAD-TRAILER for all actions, except for PutObject and UploadPart Fixes #1601 Unsigned streaming upload trailers (`STREAMING-UNSIGNED-PAYLOAD-TRAILER`) is allowed only for `PutObject` and `UploadPart`. For all other actions, the gateway now returns an `InvalidRequest` error for the `x-amz-content-sha256` header. --- s3api/middlewares/authentication.go | 5 +++ s3err/s3err.go | 6 +++ tests/integration/group-tests.go | 2 + .../unsigned_streaming_payload_trailer.go | 41 +++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index 321e40a..4208879 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -115,6 +115,11 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, region string, if !utils.IsValidSh256PayloadHeader(hashPayload) { return s3err.GetAPIError(s3err.ErrInvalidSHA256Paylod) } + // the streaming payload type is allowed only in PutObject and UploadPart + // e.g. STREAMING-UNSIGNED-PAYLOAD-TRAILER + if !streamBody && utils.IsStreamingPayload(hashPayload) { + return s3err.GetAPIError(s3err.ErrInvalidSHA256PayloadUsage) + } if streamBody { // for streaming PUT actions, authorization is deferred // until end of stream due to need to get length and diff --git a/s3err/s3err.go b/s3err/s3err.go index 70ee11e..75e3495 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -119,6 +119,7 @@ const ( ErrSignatureDoesNotMatch ErrContentSHA256Mismatch ErrInvalidSHA256Paylod + ErrInvalidSHA256PayloadUsage ErrUnsupportedAnonymousSignedStreaming ErrMissingContentLength ErrInvalidAccessKeyID @@ -510,6 +511,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER or a valid sha256 value.", HTTPStatusCode: http.StatusBadRequest, }, + ErrInvalidSHA256PayloadUsage: { + Code: "InvalidRequest", + Description: "The value of x-amz-content-sha256 header is invalid.", + HTTPStatusCode: http.StatusBadRequest, + }, ErrUnsupportedAnonymousSignedStreaming: { Code: "InvalidRequest", Description: "Anonymous requests don't support this x-amz-content-sha256 value. Please use UNSIGNED-PAYLOAD or STREAMING-UNSIGNED-PAYLOAD-TRAILER.", diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 4ee9bff..129552d 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -1109,6 +1109,7 @@ func TestUnsignedStreaminPayloadTrailer(ts *TestState) { ts.Run(UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_full_object) ts.Run(UnsignedStreamingPayloadTrailer_UploadPart_trailer_and_mp_algo_mismatch) ts.Run(UnsignedStreamingPayloadTrailer_UploadPart_success_with_trailer) + ts.Run(UnsignedStreamingPayloadTrailer_not_allowed) } } @@ -1763,5 +1764,6 @@ func GetIntTests() IntTests { "UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_full_object": UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_full_object, "UnsignedStreamingPayloadTrailer_UploadPart_trailer_and_mp_algo_mismatch": UnsignedStreamingPayloadTrailer_UploadPart_trailer_and_mp_algo_mismatch, "UnsignedStreamingPayloadTrailer_UploadPart_success_with_trailer": UnsignedStreamingPayloadTrailer_UploadPart_success_with_trailer, + "UnsignedStreamingPayloadTrailer_not_allowed": UnsignedStreamingPayloadTrailer_not_allowed, } } diff --git a/tests/integration/unsigned_streaming_payload_trailer.go b/tests/integration/unsigned_streaming_payload_trailer.go index 26a14e5..ee15716 100644 --- a/tests/integration/unsigned_streaming_payload_trailer.go +++ b/tests/integration/unsigned_streaming_payload_trailer.go @@ -2,7 +2,9 @@ package integration import ( "bytes" + "context" "fmt" + "net/http" "strings" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -514,3 +516,42 @@ func UnsignedStreamingPayloadTrailer_UploadPart_success_with_trailer(s *S3Conf) return nil }) } + +func UnsignedStreamingPayloadTrailer_not_allowed(s *S3Conf) error { + testName := "UnsignedStreamingPayloadTrailer_not_allowed" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + // doesn't matter what data is sent in the body + body := []byte("5\r\nabcde\r\n0\r\n\r\n") + // tests a couple of bucket PUT actions, where + // STREAMING-UNSIGNED-PAYLOAD-TRAILER is not allowed + for i, query := range []string{ + "cors", // PutBucketCors + "tagging", // PutBucketTagging + "object-lock", // PutObjectLockConfiguration + "ownershipControls", // PutBucketOwnership + "versioning", // PutBucketVersioning + "policy", // PutBucketPolicy + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + req, err := http.NewRequestWithContext(ctx, http.MethodPut, fmt.Sprintf("%s/%s?%s", s.endpoint, bucket, query), bytes.NewReader(body)) + if err != nil { + cancel() + return fmt.Errorf("test %v failed: %w", i+1, err) + } + + req.Header.Set("x-amz-content-sha256", "STREAMING-UNSIGNED-PAYLOAD-TRAILER") + req.Header.Set("x-amz-decoded-content-length", "5") + + _, apiErr, err := sendSignedRequest(s, req, cancel) + if err != nil { + return fmt.Errorf("test %v failed: %w", i+1, err) + } + + if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrInvalidSHA256PayloadUsage), apiErr); err != nil { + return fmt.Errorf("test %v failed: %w", i+1, err) + } + } + + return nil + }) +}