From 8b09c96cec7a1a5bbaeb1579cad5fcd0663bbf98 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 19 Oct 2026 18:32:44 -0700 Subject: [PATCH] s3: keep a signed content-length in the canonical request (#11849) Co-authored-by: Devin --- weed/s3api/auth_signature_v4.go | 6 ++++++ weed/s3api/auth_signature_v4_test.go | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index 0dc5731da7..6b6e3f05f1 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -959,6 +959,12 @@ func extractSignedHeaders(signedHeaders []string, r *http.Request, externalHost extractedSignedHeaders[header] = []string{hostHeaderValue} continue } + // The Go server strips Content-Length into r.ContentLength; clients do + // sign it, so rebuild the value the same way the client saw it. + if strings.EqualFold(header, "content-length") && r.ContentLength >= 0 { + extractedSignedHeaders[header] = []string{strconv.FormatInt(r.ContentLength, 10)} + continue + } // For all other headers we need to find them in the HTTP headers and copy them over. // We skip non-existent headers to be compatible with AWS signatures. if values, ok := reqHeaders[http.CanonicalHeaderKey(header)]; ok { diff --git a/weed/s3api/auth_signature_v4_test.go b/weed/s3api/auth_signature_v4_test.go index e2fc675915..a426357b02 100644 --- a/weed/s3api/auth_signature_v4_test.go +++ b/weed/s3api/auth_signature_v4_test.go @@ -681,6 +681,22 @@ func TestExtractSignedHeadersCase(t *testing.T) { } } +// A client may sign content-length even when the request carries no body +// (aws-cli's checksum HEAD does). The Go server strips the header into +// r.ContentLength, so extraction rebuilds it rather than dropping it. +func TestExtractSignedHeadersContentLength(t *testing.T) { + r, _ := http.NewRequest("HEAD", "http://bucket.domain.com/bucket/key", nil) + r.Host = "bucket.domain.com" + r.ContentLength = 0 + extracted, errCode := extractSignedHeaders([]string{"host", "content-length"}, r, "") + if errCode != s3err.ErrNone { + t.Fatalf("extractSignedHeaders failed: %v", errCode) + } + if got := getCanonicalHeaders(extracted); got != "content-length:0\nhost:bucket.domain.com\n" { + t.Fatalf("unexpected canonical headers %q", got) + } +} + // A router that internally rewrites the request path (the Iceberg catalog // collapses a doubled /v1 prefix) must still verify the signature against // the path the client signed, which is kept in the request context.