mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-12 23:09:33 +01:00
* s3api: verify signed request bodies against the signed payload hash A SigV4 signature covers the declared x-amz-content-sha256, but only getRequestDataReader (PutObject, UploadPart) compared the body with it. Every other handler parsed the body as received, so a signed request, DeleteObjects for example, could be replayed with a different body. After a V4 signature verifies, wrap the body so it is handed out only once all of it has been read and matched against the hash the signature covered; a decoder that stops reading early never sees unverified bytes. getRequestDataReader unwraps it and keeps verifying uploads as they stream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: answer XAmzContentSHA256Mismatch for refused DeleteObjects bodies DeleteObjects answered a body that failed its payload-hash check with InternalError and CompleteMultipartUpload with MalformedXML. Name the check that failed instead, as PutObject does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: check Content-MD5 on signed request bodies Signature V2 signs Content-MD5 rather than a payload hash, and a V4 request sent with UNSIGNED-PAYLOAD may still sign it, but bodies read outside the upload path never compared it with the body. Check it with the payload hash once a V2 or V4 signature verifies: BadDigest on a mismatch, InvalidDigest when it is malformed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: verify presigned uploads against a signed payload hash A presigned V4 URL can sign X-Amz-Content-Sha256 as a query parameter, but getRequestDataReader compared upload bodies only with the header of that name, so the signed hash went unchecked. Verify uploads against the hash the signature covered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: check Content-MD5 on bodies buffered for their payload hash IAM and other non-S3 requests signed without x-amz-content-sha256 have their body buffered and hashed during signature verification. That buffer counted as already bound, so their Content-MD5 was never checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: check Content-MD5 on directory marker bodies A PUT of a key ending in "/" stores its body on the directory entry without going through putToFiler, so its Content-MD5 was never compared with the body. Answer BadDigest on a mismatch, as putToFiler does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3api: wrap and unwrap signed bodies through helpers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
3.1 KiB
Go
107 lines
3.1 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.
|
|
// Chunked streaming uploads are decoded regardless of whether IAM is enabled;
|
|
// with no credentials to verify against, signature checking is skipped inside
|
|
// newChunkedReader.
|
|
func getRequestDataReader(s3a *S3ApiServer, r *http.Request) (io.ReadCloser, s3err.ErrorCode) {
|
|
var s3ErrCode s3err.ErrorCode
|
|
signedSha256 := unwrapSignedPayload(&r.Body)
|
|
dataReader := r.Body
|
|
rAuthType := getRequestAuthType(r)
|
|
if rAuthType == authTypeStreamingSigned || rAuthType == authTypeStreamingUnsigned {
|
|
dataReader, s3ErrCode = s3a.iam.newChunkedReader(r)
|
|
}
|
|
if s3ErrCode != s3err.ErrNone {
|
|
return nil, s3ErrCode
|
|
}
|
|
|
|
expected, valid := expectedContentSha256(r)
|
|
if !valid {
|
|
return nil, s3err.ErrInvalidArgument
|
|
}
|
|
if signedSha256 != nil {
|
|
expected = signedSha256
|
|
}
|
|
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) {
|
|
return decodeContentSha256(r.Header.Get("X-Amz-Content-Sha256"))
|
|
}
|
|
|
|
func decodeContentSha256(v string) (expected []byte, valid bool) {
|
|
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()
|
|
}
|