mirror of
https://github.com/pgsty/minio.git
synced 2026-11-01 05:12:07 +03:00
fix(auth): reject unsigned x-amz-* headers to close CopyObject confused-deputy
A presigned or signed PUT authorized for a single object could be turned
into a server-side copy of any object the signing key can read by adding
an unsigned x-amz-copy-source header, executed as the signer. SigV4
verification only walked the signed-headers list, never the headers that
actually arrived; the meta-header check matched only X-Amz-Meta- and ran
only on the presigned path, so an unsigned x-amz-* header outside the
list was never seen while the router still dispatched the PUT to
CopyObjectHandler.
Reject any x-amz-* request header not covered by the signed headers, on
both the presigned (doesPresignedSignatureMatch) and Authorization-header
(doesSignatureMatch) paths, matching AWS S3. The check tests membership
in the signed set rather than value equality, so a header whose first
value is empty (e.g. {"", "/src/secret"}) cannot slip through.
X-Amz-Content-Sha256 is exempt (payload hash: read from the query for
presigned requests and bound into the string-to-sign for signed ones, so
it is self-protected) and X-Amz-Signature-Age is exempt (an internal
scratch header written after verification, so repeated verification of
the same request stays idempotent). The synthesized X-Amz-Tagging header
in PutObjectTagging is now injected after signature verification.
Tests that previously added x-amz-copy-source and friends after signing
(relying on the vulnerable behavior) now re-sign, mirroring real S3
clients. Adds checkUnsignedHeaders unit cases and TestPresignedVerifyIdempotent.
Reported by Oren Yomtov. Inherited unchanged from upstream minio/minio.
Tracked as SN-2026-011.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -1799,6 +1799,12 @@ func testAPICopyObjectPartHandlerSanity(obj ObjectLayer, instanceType, bucketNam
|
||||
req.Header.Set("X-Amz-Copy-Source", url.QueryEscape(pathJoin(bucketName, objectName)))
|
||||
req.Header.Set("X-Amz-Copy-Source-Range", fmt.Sprintf("bytes=%d-%d", a, b))
|
||||
|
||||
// Re-sign so the copy-source x-amz-* headers are covered by the
|
||||
// signature, as real clients do; the verifier rejects unsigned x-amz-*.
|
||||
if err = signRequestV4(req, credentials.AccessKey, credentials.SecretKey); err != nil {
|
||||
t.Fatalf("Test failed to re-sign HTTP request for copy object part: <ERROR> %v", err)
|
||||
}
|
||||
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
|
||||
a = globalMinPartSize + 1
|
||||
@@ -2200,6 +2206,15 @@ func testAPICopyObjectPartHandler(obj ObjectLayer, instanceType, bucketName stri
|
||||
}
|
||||
}
|
||||
|
||||
// Re-sign so the copy-source x-amz-* headers set above are covered by
|
||||
// the signature, as real clients do; the verifier rejects unsigned
|
||||
// x-amz-* headers.
|
||||
if testCase.accessKey != "" && testCase.secretKey != "" {
|
||||
if err = signRequestV4(req, testCase.accessKey, testCase.secretKey); err != nil {
|
||||
t.Fatalf("Test %d: Failed to re-sign HTTP request for copy Object: <ERROR> %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
@@ -2626,6 +2641,16 @@ func testAPICopyObjectHandler(obj ObjectLayer, instanceType, bucketName string,
|
||||
if testCase.metadataGarbage {
|
||||
req.Header.Set("X-Amz-Metadata-Directive", "Unknown")
|
||||
}
|
||||
// The x-amz-copy-source and related x-amz-* headers set above must be
|
||||
// part of the SigV4 signature, exactly as real S3 clients send them.
|
||||
// Re-sign now that they are present; the verifier rejects unsigned
|
||||
// x-amz-* headers (an unsigned x-amz-copy-source could otherwise turn a
|
||||
// PUT grant into a server-side copy).
|
||||
if testCase.accessKey != "" && testCase.secretKey != "" {
|
||||
if err = signRequestV4(req, testCase.accessKey, testCase.secretKey); err != nil {
|
||||
t.Fatalf("Test %d: Failed to re-sign HTTP request for copy Object: <ERROR> %v", i, err)
|
||||
}
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler, `func (api objectAPIHandlers) CopyObjectHandler` handles the request.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
|
||||
Reference in New Issue
Block a user