mirror of
https://github.com/pgsty/minio.git
synced 2026-09-26 04:45:59 +03:00
fix: honor header checksum when x-amz-trailer is advertised on non-trailer chunked PUT (#107)
The AWS Java SDK v2, with chunked encoding enabled (its default), sends a PutObject as a non-trailer signed aws-chunked stream (x-amz-content-sha256: STREAMING-AWS4-HMAC-SHA256-PAYLOAD). When a checksum algorithm is set it puts the precomputed value in the x-amz-checksum-crc32 header, yet still advertises the checksum in x-amz-trailer even though no trailer chunk is ever sent. GetContentChecksum treated any x-amz-trailer-advertised checksum as trailing with an empty value, deferring it to a trailer. For the non-trailer auth type the handler sets req.Trailer = nil, so at EOF the hash.Reader looked the value up in a nil trailer, got "", and returned XAmzContentChecksumMismatch (HTTP 400) even though the correct value sat in the request header. Real S3 accepts the request, and disabling chunked encoding removed the trailer advertisement, matching the reported symptom. Honor the header value directly when a trailer-advertised checksum is already present in the request headers; fall back to trailing delivery only when the header is absent. When the header carries the checksum but it does not parse, reject the request with ErrInvalidChecksum instead of falling through to a no-validation path, so a malformed client-supplied checksum is never silently dropped. This also restores the checksum echo on the response and the stored value, while keeping genuine trailer uploads and wrong-checksum rejection intact. Fixes #107. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7qJqWwy8oFA6aCXWRzXQe Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -698,7 +698,25 @@ func GetContentChecksum(h http.Header) (*Checksum, error) {
|
||||
for _, t := range BaseChecksumTypes {
|
||||
if strings.EqualFold(t.Key(), header) {
|
||||
duplicates = res != nil
|
||||
res = NewChecksumWithType(t|ChecksumTrailing, "")
|
||||
// A checksum can be advertised via x-amz-trailer while its
|
||||
// value is still delivered in the request headers. The AWS
|
||||
// Java SDK v2 does this on chunked (aws-chunked) uploads:
|
||||
// it sends STREAMING-AWS4-HMAC-SHA256-PAYLOAD (no trailer),
|
||||
// puts the precomputed value in x-amz-checksum-*, yet still
|
||||
// lists it in x-amz-trailer, so no trailer ever arrives.
|
||||
// When the value is present as a header, honor it directly
|
||||
// instead of waiting for a trailer that will never be read.
|
||||
if v := h.Get(t.Key()); v != "" {
|
||||
res = NewChecksumWithType(t, v)
|
||||
if res == nil {
|
||||
// The value is supplied in the header but does
|
||||
// not parse. A malformed client-supplied checksum
|
||||
// is an error, not a reason to skip validation.
|
||||
return nil, ErrInvalidChecksum
|
||||
}
|
||||
} else {
|
||||
res = NewChecksumWithType(t|ChecksumTrailing, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(header), "x-amz-checksum-") && !isSupportedChecksumHeader(header) {
|
||||
|
||||
@@ -257,3 +257,72 @@ func TestChecksumSerializeDeserializeMultiPart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetContentChecksumTrailerWithHeaderValue covers the case where a checksum
|
||||
// is advertised via x-amz-trailer while its value is delivered as a request
|
||||
// header (no trailer is actually sent). The AWS Java SDK v2 does this on chunked
|
||||
// (aws-chunked) uploads that use STREAMING-AWS4-HMAC-SHA256-PAYLOAD (non-trailer)
|
||||
// but still list the checksum in x-amz-trailer. See issue #107. The header value
|
||||
// must be honored as a non-trailing checksum instead of being treated as an empty
|
||||
// trailing checksum.
|
||||
func TestGetContentChecksumTrailerWithHeaderValue(t *testing.T) {
|
||||
const crc = "Hkksgg==" // CRC32 of "Hello CRC32!"
|
||||
|
||||
// Trailer advertised AND value present in header -> non-trailing, value honored.
|
||||
h := http.Header{}
|
||||
h.Set(xhttp.AmzTrailer, xhttp.AmzChecksumCRC32)
|
||||
h.Set(xhttp.AmzChecksumCRC32, crc)
|
||||
cs, err := GetContentChecksum(h)
|
||||
if err != nil {
|
||||
t.Fatalf("GetContentChecksum error = %v, want nil", err)
|
||||
}
|
||||
if cs == nil {
|
||||
t.Fatal("GetContentChecksum returned nil checksum")
|
||||
}
|
||||
if cs.Type.Trailing() {
|
||||
t.Errorf("checksum reported as trailing; want non-trailing since value is in the header")
|
||||
}
|
||||
if !cs.Type.Is(ChecksumCRC32) {
|
||||
t.Errorf("checksum type = %s, want CRC32", cs.Type.StringFull())
|
||||
}
|
||||
if cs.Encoded != crc {
|
||||
t.Errorf("checksum value = %q, want %q", cs.Encoded, crc)
|
||||
}
|
||||
|
||||
// Trailer advertised WITHOUT a header value -> stays trailing (unchanged).
|
||||
h2 := http.Header{}
|
||||
h2.Set(xhttp.AmzTrailer, xhttp.AmzChecksumCRC32)
|
||||
cs2, err := GetContentChecksum(h2)
|
||||
if err != nil {
|
||||
t.Fatalf("GetContentChecksum (no header value) error = %v, want nil", err)
|
||||
}
|
||||
if cs2 == nil || !cs2.Type.Trailing() {
|
||||
t.Errorf("checksum = %v, want a trailing CRC32 checksum", cs2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetContentChecksumTrailerMalformedHeaderValue guards against turning a
|
||||
// malformed client-supplied checksum into a no-op. When a checksum is advertised
|
||||
// via x-amz-trailer and its header value is present but does not parse, the
|
||||
// request must be rejected (ErrInvalidChecksum) rather than silently dropped.
|
||||
// The mismatched x-amz-checksum-algorithm selector makes the regression visible:
|
||||
// without the guard, execution falls through to getContentChecksum which would
|
||||
// return (nil, nil) and install no validator at all.
|
||||
func TestGetContentChecksumTrailerMalformedHeaderValue(t *testing.T) {
|
||||
h := http.Header{}
|
||||
h.Set(xhttp.AmzTrailer, xhttp.AmzChecksumCRC32)
|
||||
h.Set(xhttp.AmzChecksumCRC32, "AQID") // decodes to 3 bytes -> invalid CRC32
|
||||
h.Set(xhttp.AmzChecksumAlgo, "SHA256")
|
||||
cs, err := GetContentChecksum(h)
|
||||
if !errors.Is(err, ErrInvalidChecksum) {
|
||||
t.Fatalf("GetContentChecksum error = %v (checksum %v), want ErrInvalidChecksum", err, cs)
|
||||
}
|
||||
|
||||
// Same, without the misleading algorithm selector: still an error.
|
||||
h2 := http.Header{}
|
||||
h2.Set(xhttp.AmzTrailer, xhttp.AmzChecksumCRC32)
|
||||
h2.Set(xhttp.AmzChecksumCRC32, "AQID")
|
||||
if _, err := GetContentChecksum(h2); !errors.Is(err, ErrInvalidChecksum) {
|
||||
t.Fatalf("GetContentChecksum (no algo selector) error = %v, want ErrInvalidChecksum", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user