mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-02 10:17:10 +01:00
fix(s3): verify SigV4 signature with decoded path behind X-Forwarded-Prefix (#12299)
* fix(s3): verify SigV4 signature with decoded path behind X-Forwarded-Prefix When SeaweedFS is hosted behind a reverse proxy that strips a URL prefix (X-Forwarded-Prefix) and forwards the percent-encoded request path, signature verification fails for object keys containing non-ASCII characters. The client signs the canonical URI as the single percent-encoded path including the prefix, but the server verifies against the already-encoded forwarded path joined to the prefix, producing a mismatch (SignatureDoesNotMatch). Decode the forwarded path and rejoin the prefix before verifying, mirroring the existing un-prefixed decoded retry. ASCII keys are unaffected because the decoded path equals the original, so the retry is skipped. Signed-off-by: Mohd Quamar Tyagi <104281681+Tyagiquamar@users.noreply.github.com> * s3: build the SigV4 candidate paths once and try them per host The forwarded-prefix and plain branches each repeated the decode-and-verify step for every host candidate. List the escaped and decoded paths once, the prefixed forms first, and verify them in the same order as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: Mohd Quamar Tyagi <104281681+Tyagiquamar@users.noreply.github.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2 files changed
+50
-23
No files matched your search
@@ -374,35 +374,29 @@ func (iam *IdentityAccessManagement) verifyV4Signature(r *http.Request, shouldCh
|
||||
if trustedPeer {
|
||||
forwardedPrefix = r.Header.Get("X-Forwarded-Prefix")
|
||||
}
|
||||
// 9. Candidate paths: the escaped path as sent, then the decoded path that
|
||||
// an S3 client encodes once when signing; each with the forwarded prefix first.
|
||||
signaturePaths := []string{pathForSignature}
|
||||
if decodedPath, decodeErr := url.PathUnescape(pathForSignature); decodeErr == nil && decodedPath != pathForSignature {
|
||||
signaturePaths = append(signaturePaths, decodedPath)
|
||||
}
|
||||
if forwardedPrefix != "" {
|
||||
prefixedPaths := make([]string, 0, 2*len(signaturePaths))
|
||||
for _, p := range signaturePaths {
|
||||
prefixedPaths = append(prefixedPaths, buildPathWithForwardedPrefix(forwardedPrefix, p))
|
||||
}
|
||||
signaturePaths = append(prefixedPaths, signaturePaths...)
|
||||
}
|
||||
var matchedHost string
|
||||
hostCandidates:
|
||||
for i, hostCandidate := range extractHostHeaderCandidates(r, iam.externalHost) {
|
||||
if i > 0 && !replaceSignedHostHeader(extractedSignedHeaders, hostCandidate) {
|
||||
break
|
||||
}
|
||||
|
||||
// 9. Verify with the X-Forwarded-Prefix path first
|
||||
if forwardedPrefix != "" {
|
||||
cleanedPath := buildPathWithForwardedPrefix(forwardedPrefix, pathForSignature)
|
||||
calculatedSignature, errCode = verify(cleanedPath)
|
||||
if errCode == s3err.ErrNone {
|
||||
for _, p := range signaturePaths {
|
||||
if calculatedSignature, errCode = verify(p); errCode == s3err.ErrNone {
|
||||
matchedHost = hostCandidate
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Verify with the original path
|
||||
calculatedSignature, errCode = verify(pathForSignature)
|
||||
if errCode == s3err.ErrNone {
|
||||
matchedHost = hostCandidate
|
||||
break
|
||||
}
|
||||
|
||||
// 11. Retry with decoded path if signature used raw path encoding
|
||||
if decodedPath, decodeErr := url.PathUnescape(pathForSignature); decodeErr == nil && decodedPath != pathForSignature {
|
||||
calculatedSignature, errCode = verify(decodedPath)
|
||||
if errCode == s3err.ErrNone {
|
||||
matchedHost = hostCandidate
|
||||
break
|
||||
break hostCandidates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -735,3 +736,35 @@ func TestVerifyV4SignatureUsesOriginalRequestPath(t *testing.T) {
|
||||
t.Fatalf("signed request rejected after path rewrite: %d", errCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignatureV4WithForwardedPrefixUnicodeKey guards against a signature
|
||||
// mismatch when an object key contains non-ASCII characters and the request is
|
||||
// received through a trusted reverse proxy that strips a path prefix. The
|
||||
// client signs the single percent-encoded path including the prefix, so the
|
||||
// server must decode the forwarded (already encoded) path before re-encoding it
|
||||
// together with the prefix.
|
||||
func TestSignatureV4WithForwardedPrefixUnicodeKey(t *testing.T) {
|
||||
iam := newTestIAMWithTrustedProxy()
|
||||
|
||||
bucket := "test-bucket"
|
||||
key := "たあいまd·.jpg"
|
||||
|
||||
// The proxy strips the X-Forwarded-Prefix and forwards the percent-encoded
|
||||
// object path (the server sees the encoded form, not the raw Unicode key).
|
||||
r, err := newTestRequest("GET", "https://example.com/"+bucket+"/"+url.PathEscape(key), 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test request: %v", err)
|
||||
}
|
||||
|
||||
r.Header.Set("X-Forwarded-Prefix", "/s3")
|
||||
r.Header.Set("Host", "example.com")
|
||||
r.RemoteAddr = "10.0.0.1:1234"
|
||||
|
||||
// The client signs the full URL path including the prefix, using the single
|
||||
// percent-encoded form of the Unicode key.
|
||||
signV4WithPath(r, "AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "/s3/"+bucket+"/"+key)
|
||||
|
||||
if _, _, errCode := iam.doesSignatureMatch(r); errCode != s3err.ErrNone {
|
||||
t.Fatalf("signature verification failed for unicode key behind forwarded prefix: code %d", int(errCode))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user