From 689b5b61bfd515dbbbd9e66d2c4b32ed864df761 Mon Sep 17 00:00:00 2001 From: 7y-9 Date: Thu, 11 Jun 2026 02:00:35 +0800 Subject: [PATCH] fix(s3api): reject empty v4 signed header names (#9910) Problem: Signature V4 SignedHeaders parsing accepted empty header name segments such as host; or ;host. Malformed Authorization headers could continue into signature verification instead of failing during header parsing. Root cause: parseSignedHeader only checked that the SignedHeaders value was non-empty, then split it on semicolons without validating each element. Fix: reject empty or whitespace-only signed header elements with ErrMissingFields before returning the parsed header list. Reproduction: go test ./weed/s3api -run TestParseSignedHeaderRejectsEmptyHeaderNames -count=1 failed before the fix because SignedHeaders=host; returned ErrNone. Validation: gofmt -w weed/s3api/auth_signature_v4.go weed/s3api/auth_signature_v4_test.go; git diff --check; go test ./weed/s3api -run TestParseSignedHeaderRejectsEmptyHeaderNames -count=1; go test ./weed/s3api -count=1 Co-authored-by: Codex --- weed/s3api/auth_signature_v4.go | 19 ++++++++++-- weed/s3api/auth_signature_v4_test.go | 45 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index 7d3705257..a6a763f78 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -559,6 +559,10 @@ func extractV4AuthInfoFromQuery(r *http.Request) (*v4AuthInfo, s3err.ErrorCode) if query.Get("X-Amz-Expires") == "" { return nil, s3err.ErrInvalidQueryParams } + signedHeaders, errCode := parseSignedHeaderList(query.Get("X-Amz-SignedHeaders")) + if errCode != s3err.ErrNone { + return nil, errCode + } // Parse date dateStr := query.Get("X-Amz-Date") @@ -584,7 +588,7 @@ func extractV4AuthInfoFromQuery(r *http.Request) (*v4AuthInfo, s3err.ErrorCode) return &v4AuthInfo{ Signature: query.Get("X-Amz-Signature"), AccessKey: credHeader.accessKey, - SignedHeaders: strings.Split(query.Get("X-Amz-SignedHeaders"), ";"), + SignedHeaders: signedHeaders, Date: t, Region: credHeader.scope.region, Service: credHeader.scope.service, @@ -716,10 +720,19 @@ func parseSignedHeader(signedHdrElement string) ([]string, s3err.ErrorCode) { if signedHdrFields[0] != "SignedHeaders" { return nil, s3err.ErrMissingSignHeadersTag } - if signedHdrFields[1] == "" { + return parseSignedHeaderList(signedHdrFields[1]) +} + +func parseSignedHeaderList(signedHeadersValue string) ([]string, s3err.ErrorCode) { + if signedHeadersValue == "" { return nil, s3err.ErrMissingFields } - signedHeaders := strings.Split(signedHdrFields[1], ";") + signedHeaders := strings.Split(signedHeadersValue, ";") + for _, header := range signedHeaders { + if strings.TrimSpace(header) == "" { + return nil, s3err.ErrMissingFields + } + } return signedHeaders, s3err.ErrNone } diff --git a/weed/s3api/auth_signature_v4_test.go b/weed/s3api/auth_signature_v4_test.go index a5f66e558..a13740a8b 100644 --- a/weed/s3api/auth_signature_v4_test.go +++ b/weed/s3api/auth_signature_v4_test.go @@ -66,6 +66,51 @@ func TestExtractV4AuthInfoFromHeader_S3Tables(t *testing.T) { } } +func TestParseSignedHeaderRejectsEmptyHeaderNames(t *testing.T) { + cases := []string{ + "SignedHeaders=host;", + "SignedHeaders=;host", + "SignedHeaders=host;;x-amz-date", + } + + for _, tc := range cases { + if _, errCode := parseSignedHeader(tc); errCode != s3err.ErrMissingFields { + t.Fatalf("parseSignedHeader(%q) errCode = %v, want %v", tc, errCode, s3err.ErrMissingFields) + } + } +} + +func TestExtractV4AuthInfoFromQueryRejectsEmptySignedHeaderNames(t *testing.T) { + now := time.Now().UTC() + cases := []string{ + "host;", + ";host", + "host;;x-amz-date", + } + + for _, tc := range cases { + t.Run(tc, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "http://localhost/bucket/object", nil) + if err != nil { + t.Fatalf("NewRequest: %v", err) + } + + query := req.URL.Query() + query.Set("X-Amz-Algorithm", signV4Algorithm) + query.Set("X-Amz-Credential", fmt.Sprintf("AKIAIOSFODNN7EXAMPLE/%s/us-east-1/s3/aws4_request", now.Format(yyyymmdd))) + query.Set("X-Amz-Date", now.Format(iso8601Format)) + query.Set("X-Amz-Expires", "60") + query.Set("X-Amz-Signature", "dummy") + query.Set("X-Amz-SignedHeaders", tc) + req.URL.RawQuery = query.Encode() + + if _, errCode := extractV4AuthInfoFromQuery(req); errCode != s3err.ErrMissingFields { + t.Fatalf("extractV4AuthInfoFromQuery(%q) errCode = %v, want %v", tc, errCode, s3err.ErrMissingFields) + } + }) + } +} + func TestBuildPathWithForwardedPrefix(t *testing.T) { tests := []struct { name string