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 <noreply@openai.com>
This commit is contained in:
7y-9
2026-06-10 11:00:35 -07:00
committed by GitHub
co-authored by Codex
parent 7bf2dfc9ab
commit 689b5b61bf
2 changed files with 61 additions and 3 deletions
+16 -3
View File
@@ -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
}
+45
View File
@@ -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