Browse Source

s3api: preserve Host header port in signature verification (#8434)

Avoid stripping default ports (80/443) from the Host header in extractHostHeader.
This fixes SignatureDoesNotMatch errors when SeaweedFS is accessed via a proxy
(like Kong Ingress) that explicitly includes the port in the Host header or
X-Forwarded-Host, which S3 clients sign.

Also cleaned up unused variables and logic after refactoring.
codex/list-object-versions-newest-first
Chris Lu 21 hours ago
committed by GitHub
parent
commit
98d89ffad7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 50
      weed/s3api/auth_signature_v4.go
  2. 24
      weed/s3api/auth_signature_v4_test.go
  3. 8
      weed/s3api/auto_signature_v4_test.go

50
weed/s3api/auth_signature_v4.go

@ -788,25 +788,9 @@ func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header,
func extractHostHeader(r *http.Request) string {
forwardedHost := r.Header.Get("X-Forwarded-Host")
forwardedPort := r.Header.Get("X-Forwarded-Port")
forwardedProto := r.Header.Get("X-Forwarded-Proto")
// Determine the effective scheme with correct order of precedence:
// 1. X-Forwarded-Proto (most authoritative, reflects client's original protocol)
// 2. r.TLS (authoritative for direct connection to server)
// 3. r.URL.Scheme (fallback, may not always be set correctly)
// 4. Default to "http"
scheme := "http"
if r.URL.Scheme != "" {
scheme = r.URL.Scheme
}
if r.TLS != nil {
scheme = "https"
}
if forwardedProto != "" {
scheme = forwardedProto
}
var host, port string
explicitPort := false
if forwardedHost != "" {
// X-Forwarded-Host can be a comma-separated list of hosts when there are multiple proxies.
// Use only the first host in the list and trim spaces for robustness.
@ -815,12 +799,16 @@ func extractHostHeader(r *http.Request) string {
} else {
host = strings.TrimSpace(forwardedHost)
}
// Baseline port from forwarded port if available
if forwardedPort != "" {
port = forwardedPort
explicitPort = true
}
// If the host itself contains a port, it should take precedence
if h, p, err := net.SplitHostPort(host); err == nil {
host = h
port = p
}
if forwardedPort != "" && isDefaultPort(scheme, port) {
port = forwardedPort
explicitPort = true
}
} else {
host = r.Host
@ -830,12 +818,13 @@ func extractHostHeader(r *http.Request) string {
if h, p, err := net.SplitHostPort(host); err == nil {
host = h
port = p
explicitPort = true
}
}
// If we have a non-default port, join it with the host.
// If a port was explicitly provided, join it with the host.
// net.JoinHostPort will handle bracketing for IPv6.
if port != "" && !isDefaultPort(scheme, port) {
if explicitPort && port != "" {
// Strip existing brackets before calling JoinHostPort, which automatically adds
// brackets for IPv6 addresses. This prevents double-bracketing like [[::1]]:8080.
// Using Trim handles both well-formed and malformed bracketed hosts.
@ -843,7 +832,7 @@ func extractHostHeader(r *http.Request) string {
return net.JoinHostPort(host, port)
}
// No port or default port was stripped. According to AWS SDK behavior (aws-sdk-go-v2),
// No explicit port was provided (or port was empty). According to AWS SDK behavior (aws-sdk-go-v2),
// when a default port is removed from an IPv6 address, the brackets should also be removed.
// This matches AWS S3 signature calculation requirements.
// Reference: https://github.com/aws/aws-sdk-go-v2/blob/main/aws/signer/internal/v4/host.go
@ -855,21 +844,6 @@ func extractHostHeader(r *http.Request) string {
return host
}
func isDefaultPort(scheme, port string) bool {
if port == "" {
return true
}
switch port {
case "80":
return strings.EqualFold(scheme, "http")
case "443":
return strings.EqualFold(scheme, "https")
default:
return false
}
}
// getScope generate a string of a specific date, an AWS region, and a service.
func getScope(t time.Time, region string, service string) string {
scope := strings.Join([]string{

24
weed/s3api/auth_signature_v4_test.go

@ -208,7 +208,7 @@ func TestExtractHostHeader(t *testing.T) {
forwardedHost: "example.com",
forwardedPort: "80",
forwardedProto: "http",
expected: "example.com",
expected: "example.com:80",
},
{
name: "X-Forwarded-Host with X-Forwarded-Port (HTTPS standard port 443)",
@ -216,7 +216,7 @@ func TestExtractHostHeader(t *testing.T) {
forwardedHost: "example.com",
forwardedPort: "443",
forwardedProto: "https",
expected: "example.com",
expected: "example.com:443",
},
// Issue #6649: X-Forwarded-Host already contains port (Traefik/HAProxy style)
{
@ -227,6 +227,14 @@ func TestExtractHostHeader(t *testing.T) {
forwardedProto: "https",
expected: "127.0.0.1:8433",
},
{
name: "X-Forwarded-Host with standard port already included (HTTPS 443)",
hostHeader: "backend:8333",
forwardedHost: "example.com:443",
forwardedPort: "443",
forwardedProto: "https",
expected: "example.com:443",
},
{
name: "X-Forwarded-Host with port, no X-Forwarded-Port header",
hostHeader: "backend:8333",
@ -253,20 +261,20 @@ func TestExtractHostHeader(t *testing.T) {
expected: "[::1]:8080",
},
{
name: "IPv6 address without brackets and standard port, should strip brackets per AWS SDK",
name: "IPv6 address without brackets and standard port, should include brackets and port when explicit",
hostHeader: "backend:8333",
forwardedHost: "::1",
forwardedPort: "80",
forwardedProto: "http",
expected: "::1",
expected: "[::1]:80",
},
{
name: "IPv6 address without brackets and standard HTTPS port, should strip brackets per AWS SDK",
name: "IPv6 address without brackets and standard HTTPS port, should include brackets and port when explicit",
hostHeader: "backend:8333",
forwardedHost: "2001:db8::1",
forwardedPort: "443",
forwardedProto: "https",
expected: "2001:db8::1",
expected: "[2001:db8::1]:443",
},
{
name: "IPv6 address with brackets but no port, should add port",
@ -277,12 +285,12 @@ func TestExtractHostHeader(t *testing.T) {
expected: "[2001:db8::1]:8080",
},
{
name: "IPv6 full address with brackets and default port (should strip port and brackets)",
name: "IPv6 full address with brackets and default port (should preserve port if explicit)",
hostHeader: "backend:8333",
forwardedHost: "[2001:db8:85a3::8a2e:370:7334]:443",
forwardedPort: "443",
forwardedProto: "https",
expected: "2001:db8:85a3::8a2e:370:7334",
expected: "[2001:db8:85a3::8a2e:370:7334]:443",
},
{
name: "IPv4-mapped IPv6 address without brackets, should add brackets with port",

8
weed/s3api/auto_signature_v4_test.go

@ -415,13 +415,13 @@ func TestSignatureV4WithoutProxy(t *testing.T) {
name: "HTTP with standard port",
host: "backend:80",
proto: "http",
expectedHost: "backend",
expectedHost: "backend:80",
},
{
name: "HTTPS with standard port",
host: "backend:443",
proto: "https",
expectedHost: "backend",
expectedHost: "backend:443",
},
{
name: "HTTP without port",
@ -451,13 +451,13 @@ func TestSignatureV4WithoutProxy(t *testing.T) {
name: "IPv6 HTTP with standard port",
host: "[::1]:80",
proto: "http",
expectedHost: "::1",
expectedHost: "[::1]:80",
},
{
name: "IPv6 HTTPS with standard port",
host: "[::1]:443",
proto: "https",
expectedHost: "::1",
expectedHost: "[::1]:443",
},
{
name: "IPv6 HTTP without port",

Loading…
Cancel
Save