diff --git a/s3api/middlewares/public-bucket.go b/s3api/middlewares/public-bucket.go index 4b3634f..b3adcea 100644 --- a/s3api/middlewares/public-bucket.go +++ b/s3api/middlewares/public-bucket.go @@ -15,6 +15,8 @@ package middlewares import ( + "crypto/sha256" + "encoding/hex" "io" "strings" @@ -60,9 +62,19 @@ func AuthorizePublicBucketAccess(be backend.Backend, s3action string, policyPerm return err } + // at this point the bucket is considered as public + // as public access is granted + utils.ContextKeyPublicBucket.Set(ctx, true) + + payloadHash := ctx.Get("X-Amz-Content-Sha256") + err = utils.IsAnonymousPayloadHashSupported(payloadHash) + if err != nil { + return err + } + if streamBody { - payloadType := ctx.Get("X-Amz-Content-Sha256") - if utils.IsUnsignedStreamingPayload(payloadType) { + if utils.IsUnsignedStreamingPayload(payloadHash) { + // stack an unsigned streaming payload reader checksumType, err := utils.ExtractChecksumType(ctx) if err != nil { return err @@ -73,16 +85,32 @@ func AuthorizePublicBucketAccess(be backend.Backend, s3action string, policyPerm cr, err = utils.NewUnsignedChunkReader(r, checksumType) return cr }) - if err != nil { - return err - } - } else { - utils.ContextKeyBodyReader.Set(ctx, ctx.Request().BodyStream()) - } + return err + } else if utils.IsUnsignedPaylod(payloadHash) { + // for UNSIGNED-PAYLOD simply store the body reader in context locals + utils.ContextKeyBodyReader.Set(ctx, ctx.Request().BodyStream()) + return nil + } else { + // stack a hash reader to calculated the payload sha256 hash + wrapBodyReader(ctx, func(r io.Reader) io.Reader { + var cr io.Reader + cr, err = utils.NewHashReader(r, payloadHash, utils.HashTypeSha256Hex) + return cr + }) + + return err + } } - utils.ContextKeyPublicBucket.Set(ctx, true) + // Calculate the hash of the request payload + hashedPayload := sha256.Sum256(ctx.Body()) + hexPayload := hex.EncodeToString(hashedPayload[:]) + + // Compare the calculated hash with the hash provided + if payloadHash != hexPayload { + return s3err.GetAPIError(s3err.ErrContentSHA256Mismatch) + } return nil } diff --git a/s3api/utils/chunk-reader.go b/s3api/utils/chunk-reader.go index 19625ff..15f1321 100644 --- a/s3api/utils/chunk-reader.go +++ b/s3api/utils/chunk-reader.go @@ -133,6 +133,25 @@ func IsUnsignedStreamingPayload(str string) bool { return payloadType(str) == payloadTypeStreamingUnsignedTrailer } +// IsAnonymousPayloadHashSupported returns error if payload hash +// is streaming signed. +// e.g. +// "STREAMING-AWS4-HMAC-SHA256-PAYLOAD", "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD" ... +func IsAnonymousPayloadHashSupported(hash string) error { + switch payloadType(hash) { + case payloadTypeStreamingEcdsa, payloadTypeStreamingEcdsaTrailer, payloadTypeStreamingSigned, payloadTypeStreamingSignedTrailer: + return s3err.GetAPIError(s3err.ErrUnsupportedAnonymousSignedStreaming) + } + + return nil +} + +// IsUnsignedPaylod checks if the provided payload hash type +// is "UNSIGNED-PAYLOAD" +func IsUnsignedPaylod(hash string) bool { + return hash == string(payloadTypeUnsigned) +} + // IsChunkEncoding checks for streaming/unsigned authorization types func IsStreamingPayload(str string) bool { pt := payloadType(str) diff --git a/s3err/s3err.go b/s3err/s3err.go index 91e4bb2..65f1c5b 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -116,6 +116,7 @@ const ( ErrSignatureDoesNotMatch ErrContentSHA256Mismatch ErrInvalidSHA256Paylod + ErrUnsupportedAnonymousSignedStreaming ErrMissingContentLength ErrInvalidAccessKeyID ErrRequestNotReadyYet @@ -481,6 +482,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, }, + ErrUnsupportedAnonymousSignedStreaming: { + Code: "InvalidRequest", + Description: "Anonymous requests don't support this x-amz-content-sha256 value. Please use UNSIGNED-PAYLOAD or STREAMING-UNSIGNED-PAYLOAD-TRAILER.", + HTTPStatusCode: http.StatusBadRequest, + }, ErrMissingContentLength: { Code: "MissingContentLength", Description: "You must provide the Content-Length HTTP header.", diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index a1aae18..a0822fc 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -926,6 +926,8 @@ func TestPublicBuckets(s *S3Conf) { PublicBucket_public_bucket_policy(s) PublicBucket_public_object_policy(s) PublicBucket_public_acl(s) + PublicBucket_signed_streaming_payload(s) + PublicBucket_incorrect_sha256_hash(s) } func TestVersioning(s *S3Conf) { @@ -1534,6 +1536,8 @@ func GetIntTests() IntTests { "PublicBucket_public_bucket_policy": PublicBucket_public_bucket_policy, "PublicBucket_public_object_policy": PublicBucket_public_object_policy, "PublicBucket_public_acl": PublicBucket_public_acl, + "PublicBucket_signed_streaming_payload": PublicBucket_signed_streaming_payload, + "PublicBucket_incorrect_sha256_hash": PublicBucket_incorrect_sha256_hash, "PutBucketVersioning_non_existing_bucket": PutBucketVersioning_non_existing_bucket, "PutBucketVersioning_invalid_status": PutBucketVersioning_invalid_status, "PutBucketVersioning_success_enabled": PutBucketVersioning_success_enabled, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index b15aa96..8471622 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -21103,6 +21103,57 @@ func PublicBucket_public_acl(s *S3Conf) error { }, withAnonymousClient(), withOwnership(types.ObjectOwnershipBucketOwnerPreferred)) } +func PublicBucket_signed_streaming_payload(s *S3Conf) error { + testName := "PublicBucket_signed_streaming_payload" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + err := grantPublicBucketPolicy(s3client, bucket, policyTypeFull) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s/%s", s.endpoint, bucket, "obj"), nil) + if err != nil { + return err + } + + req.Header.Add("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD") + + resp, err := s.httpClient.Do(req) + if err != nil { + return err + } + + return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrUnsupportedAnonymousSignedStreaming)) + }) +} + +func PublicBucket_incorrect_sha256_hash(s *S3Conf) error { + testName := "PublicBucket_incorrect_sha256_hash" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + err := grantPublicBucketPolicy(s3client, bucket, policyTypeFull) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s/%s", s.endpoint, bucket, "obj"), nil) + if err != nil { + return err + } + + // in anonymous requests the sha256 hash validity is not checked + // so for any invalid values, the server calculates the hash + // and compares with the provided one + req.Header.Add("x-amz-content-sha256", "incorrect_hash") + + resp, err := s.httpClient.Do(req) + if err != nil { + return err + } + + return checkHTTPResponseApiErr(resp, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch)) + }) +} + // IAM related tests // multi-user iam tests func IAM_user_access_denied(s *S3Conf) error { diff --git a/tests/integration/utils.go b/tests/integration/utils.go index 5ee4d5d..6367e84 100644 --- a/tests/integration/utils.go +++ b/tests/integration/utils.go @@ -1064,8 +1064,7 @@ func grantPublicBucketPolicy(client *s3.Client, bucket string, tp policyType) er case policyTypeObject: doc = genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket)) case policyTypeFull: - template := ` - { + template := `{ "Statement": [ { "Effect": "Allow",