Merge pull request #1555 from versity/sis/public-buckets-sha256-payload

fix: adds checks for x-amz-content-sha256 in anonymous requests
This commit is contained in:
Ben McClelland
2025-09-22 21:58:40 -07:00
committed by GitHub
6 changed files with 118 additions and 11 deletions
+37 -9
View File
@@ -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
}
+19
View File
@@ -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)
+6
View File
@@ -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.",
+4
View File
@@ -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,
+51
View File
@@ -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 {
+1 -2
View File
@@ -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",