S3: support for the X-Forwarded-Prefix header (#7068)

* support for the X-Forwarded-Prefix header

* remove comments

* refactoring

* refactoring

* path.Clean
This commit is contained in:
Chris Lu
2025-08-01 13:07:54 -07:00
committed by GitHub
parent 52d87f1d29
commit f1eb4dd427
2 changed files with 328 additions and 23 deletions

View File

@@ -24,6 +24,7 @@ import (
"crypto/subtle"
"encoding/hex"
"net/http"
"path"
"regexp"
"sort"
"strconv"
@@ -154,13 +155,14 @@ func (iam *IdentityAccessManagement) doesSignatureMatch(hashedPayload string, r
}
bucket, object := s3_constants.GetBucketAndObject(r)
if !identity.canDo(s3_constants.ACTION_WRITE, bucket, object) {
canDoResult := identity.canDo(s3_constants.ACTION_WRITE, bucket, object)
if !canDoResult {
return nil, s3err.ErrAccessDenied
}
// Extract date, if not present throw error.
var dateStr string
if dateStr = req.Header.Get(http.CanonicalHeaderKey("x-amz-date")); dateStr == "" {
if dateStr = req.Header.Get("x-amz-date"); dateStr == "" {
if dateStr = r.Header.Get("Date"); dateStr == "" {
return nil, s3err.ErrMissingDateHeader
}
@@ -174,25 +176,67 @@ func (iam *IdentityAccessManagement) doesSignatureMatch(hashedPayload string, r
// Query string.
queryStr := req.URL.Query().Encode()
// Check if reverse proxy is forwarding with prefix
if forwardedPrefix := r.Header.Get("X-Forwarded-Prefix"); forwardedPrefix != "" {
// Try signature verification with the forwarded prefix first.
// This handles cases where reverse proxies strip URL prefixes and add the X-Forwarded-Prefix header.
errCode = iam.verifySignatureWithPath(extractedSignedHeaders, hashedPayload, queryStr, path.Clean(forwardedPrefix+req.URL.Path), req.Method, foundCred.SecretKey, t, signV4Values)
if errCode == s3err.ErrNone {
return identity, errCode
}
}
// Try normal signature verification (without prefix)
errCode = iam.verifySignatureWithPath(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method, foundCred.SecretKey, t, signV4Values)
if errCode == s3err.ErrNone {
return identity, errCode
}
return nil, errCode
}
// verifySignatureWithPath verifies signature with a given path (used for both normal and prefixed paths).
func (iam *IdentityAccessManagement) verifySignatureWithPath(extractedSignedHeaders http.Header, hashedPayload, queryStr, urlPath, method, secretKey string, t time.Time, signV4Values signValues) s3err.ErrorCode {
// Get canonical request.
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method)
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, urlPath, method)
// Get string to sign from canonical request.
stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope())
// Get hmac signing key.
signingKey := getSigningKey(foundCred.SecretKey, signV4Values.Credential.scope.date.Format(yyyymmdd), signV4Values.Credential.scope.region, "s3")
signingKey := getSigningKey(secretKey, signV4Values.Credential.scope.date.Format(yyyymmdd), signV4Values.Credential.scope.region, "s3")
// Calculate signature.
newSignature := getSignature(signingKey, stringToSign)
// Verify if signature match.
if !compareSignatureV4(newSignature, signV4Values.Signature) {
return nil, s3err.ErrSignatureDoesNotMatch
return s3err.ErrSignatureDoesNotMatch
}
// Return error none.
return identity, s3err.ErrNone
return s3err.ErrNone
}
// verifyPresignedSignatureWithPath verifies presigned signature with a given path (used for both normal and prefixed paths).
func (iam *IdentityAccessManagement) verifyPresignedSignatureWithPath(extractedSignedHeaders http.Header, hashedPayload, queryStr, urlPath, method, secretKey string, t time.Time, credHeader credentialHeader, signature string) s3err.ErrorCode {
// Get canonical request.
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, urlPath, method)
// Get string to sign from canonical request.
stringToSign := getStringToSign(canonicalRequest, t, credHeader.getScope())
// Get hmac signing key.
signingKey := getSigningKey(secretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, "s3")
// Calculate expected signature.
expectedSignature := getSignature(signingKey, stringToSign)
// Verify if signature match.
if !compareSignatureV4(expectedSignature, signature) {
return s3err.ErrSignatureDoesNotMatch
}
return s3err.ErrNone
}
// Simple implementation for presigned signature verification
@@ -284,24 +328,24 @@ func (iam *IdentityAccessManagement) doesPresignedSignatureMatch(hashedPayload s
queryForCanonical.Del("X-Amz-Signature")
queryStr := strings.Replace(queryForCanonical.Encode(), "+", "%20", -1)
// Get canonical request
canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, r.URL.Path, r.Method)
// Get string to sign
stringToSign := getStringToSign(canonicalRequest, t, credHeader.getScope())
// Get signing key
signingKey := getSigningKey(foundCred.SecretKey, credHeader.scope.date.Format(yyyymmdd), credHeader.scope.region, "s3")
// Calculate expected signature
expectedSignature := getSignature(signingKey, stringToSign)
// Verify signature
if !compareSignatureV4(expectedSignature, signature) {
return nil, s3err.ErrSignatureDoesNotMatch
var errCode s3err.ErrorCode
// Check if reverse proxy is forwarding with prefix for presigned URLs
if forwardedPrefix := r.Header.Get("X-Forwarded-Prefix"); forwardedPrefix != "" {
// Try signature verification with the forwarded prefix first.
// This handles cases where reverse proxies strip URL prefixes and add the X-Forwarded-Prefix header.
errCode = iam.verifyPresignedSignatureWithPath(extractedSignedHeaders, hashedPayload, queryStr, path.Clean(forwardedPrefix+r.URL.Path), r.Method, foundCred.SecretKey, t, credHeader, signature)
if errCode == s3err.ErrNone {
return identity, errCode
}
}
return identity, s3err.ErrNone
// Try normal signature verification (without prefix)
errCode = iam.verifyPresignedSignatureWithPath(extractedSignedHeaders, hashedPayload, queryStr, r.URL.Path, r.Method, foundCred.SecretKey, t, credHeader, signature)
if errCode == s3err.ErrNone {
return identity, errCode
}
return nil, errCode
}
// credentialHeader data type represents structured form of Credential
@@ -444,6 +488,12 @@ func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header,
// extractHostHeader returns the value of host header if available.
func extractHostHeader(r *http.Request) string {
// Check for X-Forwarded-Host header first, which is set by reverse proxies
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
// Using reverse proxy with X-Forwarded-Host.
return forwardedHost
}
hostHeaderValue := r.Host
// For standard requests, this should be fine.
if r.Host != "" {