mirror of
https://github.com/versity/versitygw.git
synced 2026-08-15 11:46:14 +00:00
fix: adds checks for x-amz-content-sha256 in anonymous requests
Fixes #1554 Fixes #1423 The gateway previously ignored the `x-amz-content-sha256` header for anonymous unsigned requests to public buckets. This PR adds hash calculation for this header and correctly handles special payload types. It also fixes the case where a signed streaming payload (`STREAMING-AWS4-HMAC-SHA256-PAYLOAD...`) is used with anonymous requests. In this scenario, the gateway now returns a specific "not supported" error, consistent with S3 behavior.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user