Files
seaweedfs/weed/s3api/s3api_put_object_helper.go
T
Chris LuandDevin f9a70da28b s3: return InvalidArgument for malformed x-amz-content-sha256
A header value that decodes to neither 32-byte hex nor base64 is a
malformed argument, not a hash mismatch.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-07 05:10:11 -07:00

112 lines
3.4 KiB
Go

package s3api
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"hash"
"io"
"net/http"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
// getRequestDataReader returns the appropriate reader for the request body.
// When IAM is disabled, it still processes chunked transfer encoding for
// authTypeStreamingUnsigned to strip checksum headers and extract the actual data.
// This fixes issues where chunked data with checksums would be stored incorrectly
// when IAM is not enabled.
func getRequestDataReader(s3a *S3ApiServer, r *http.Request) (io.ReadCloser, s3err.ErrorCode) {
var s3ErrCode s3err.ErrorCode
dataReader := r.Body
rAuthType := getRequestAuthType(r)
if s3a.iam.isEnabled() {
if rAuthType == authTypeStreamingSigned || rAuthType == authTypeStreamingUnsigned {
dataReader, s3ErrCode = s3a.iam.newChunkedReader(r)
}
} else {
switch rAuthType {
case authTypeStreamingSigned:
s3ErrCode = s3err.ErrAuthNotSetup
case authTypeStreamingUnsigned:
// Even when IAM is disabled, we still need to handle chunked transfer encoding
// to strip checksum headers and process the data correctly
dataReader, s3ErrCode = s3a.iam.newChunkedReader(r)
}
}
if s3ErrCode != s3err.ErrNone {
return nil, s3ErrCode
}
expected, valid := expectedContentSha256(r)
if !valid {
return nil, s3err.ErrInvalidArgument
}
if expected != nil && dataReader != nil {
if r.ContentLength == 0 {
// Handlers may never read an empty body, so check it now.
emptySum := sha256.Sum256(nil)
if !bytes.Equal(expected, emptySum[:]) {
return nil, s3err.ErrContentSHA256Mismatch
}
return dataReader, s3err.ErrNone
}
dataReader = &contentSha256Verifier{reader: dataReader, hasher: sha256.New(), expected: expected}
}
return dataReader, s3err.ErrNone
}
// expectedContentSha256 decodes the x-amz-content-sha256 header when it declares
// a payload hash — hex or base64 per SigV4 — so the body can be checked as it
// streams. Sentinel values (streaming, unsigned) carry no hash and are skipped;
// anything that decodes to neither returns valid=false.
func expectedContentSha256(r *http.Request) (expected []byte, valid bool) {
v := r.Header.Get("X-Amz-Content-Sha256")
if v == "" || v == unsignedPayload || v == streamingContentSHA256 ||
v == streamingContentSHA256Trailer || v == streamingUnsignedPayload {
return nil, true
}
if decoded, err := hex.DecodeString(v); err == nil && len(decoded) == sha256.Size {
return decoded, true
}
if decoded, err := base64.StdEncoding.DecodeString(v); err == nil && len(decoded) == sha256.Size {
return decoded, true
}
return nil, false
}
// contentSha256Verifier checks the decoded x-amz-content-sha256 once the stream
// is exhausted; handlers that never consume the body are unaffected.
type contentSha256Verifier struct {
reader io.ReadCloser
hasher hash.Hash
expected []byte
done bool
pending error
}
func (v *contentSha256Verifier) Read(p []byte) (int, error) {
if v.pending != nil {
return 0, v.pending
}
n, err := v.reader.Read(p)
v.hasher.Write(p[:n])
if err == io.EOF && !v.done {
v.done = true
if !bytes.Equal(v.hasher.Sum(nil), v.expected) {
v.pending = errors.New(s3err.ErrMsgContentSha256Mismatch)
err = nil
}
}
if n == 0 && v.pending != nil {
return 0, v.pending
}
return n, err
}
func (v *contentSha256Verifier) Close() error {
return v.reader.Close()
}