diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index b2067d5a00..1cd3268edd 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -151,8 +151,20 @@ func parseSignV4(v4Auth string) (sv signValues, aec s3err.ErrorCode) { // Strip off the Algorithm prefix. v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm) authFields := strings.Split(strings.TrimSpace(v4Auth), ",") - if len(authFields) != 3 { - return sv, s3err.ErrMissingFields + malformed := len(authFields) != 3 + var hasCredential, hasSignature bool + for _, field := range authFields { + field = strings.TrimSpace(field) + malformed = malformed || field == "" + hasCredential = hasCredential || strings.HasPrefix(field, "Credential=") + hasSignature = hasSignature || strings.HasPrefix(field, "Signature=") + } + if malformed { + // AWS distinguishes which required field is absent. + if !hasCredential { + return sv, s3err.ErrInvalidArgument + } + return sv, s3err.ErrAuthorizationHeaderMalformed } // Initialize signature version '4' structured header. @@ -744,13 +756,13 @@ func parseCredentialHeader(credElement string) (ch credentialHeader, aec s3err.E func parseSignature(signElement string) (string, s3err.ErrorCode) { signFields := strings.Split(strings.TrimSpace(signElement), "=") if len(signFields) != 2 { - return "", s3err.ErrMissingFields + return "", s3err.ErrAuthorizationHeaderMalformed } if signFields[0] != "Signature" { return "", s3err.ErrMissingSignTag } if signFields[1] == "" { - return "", s3err.ErrMissingFields + return "", s3err.ErrAuthorizationHeaderMalformed } signature := signFields[1] return signature, s3err.ErrNone diff --git a/weed/s3api/auth_signature_v4_unsigned_headers_test.go b/weed/s3api/auth_signature_v4_unsigned_headers_test.go index a9ccdb72f1..49bbe65643 100644 --- a/weed/s3api/auth_signature_v4_unsigned_headers_test.go +++ b/weed/s3api/auth_signature_v4_unsigned_headers_test.go @@ -360,3 +360,29 @@ func TestPresignedPutRejectsUnsignedMetadataHeaders(t *testing.T) { }) } } + +// TestParseSignV4MissingFields exercises how a truncated Authorization header +// maps to the AWS error code for the absent field. +func TestParseSignV4MissingFields(t *testing.T) { + valid := "AWS4-HMAC-SHA256 Credential=AKIA/20260101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=0123456789abcdef" + + tests := []struct { + name string + auth string + want s3err.ErrorCode + }{ + {"valid", valid, s3err.ErrNone}, + {"no signature field", "AWS4-HMAC-SHA256 Credential=AKIA/20260101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date", s3err.ErrAuthorizationHeaderMalformed}, + {"empty signature field", "AWS4-HMAC-SHA256 Credential=AKIA/20260101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, ", s3err.ErrAuthorizationHeaderMalformed}, + {"no credential field", "AWS4-HMAC-SHA256 SignedHeaders=host;x-amz-date, Signature=0123456789abcdef", s3err.ErrInvalidArgument}, + {"leading comma after dropping credential", "AWS4-HMAC-SHA256 , SignedHeaders=host;x-amz-date, Signature=0123456789abcdef", s3err.ErrInvalidArgument}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, got := parseSignV4(tt.auth) + if got != tt.want { + t.Fatalf("parseSignV4() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/weed/s3api/s3err/s3api_errors.go b/weed/s3api/s3err/s3api_errors.go index ad5c0d3bef..a24bd0efdf 100644 --- a/weed/s3api/s3err/s3api_errors.go +++ b/weed/s3api/s3err/s3api_errors.go @@ -486,7 +486,7 @@ var errorCodeResponse = map[ErrorCode]APIError{ HTTPStatusCode: http.StatusBadRequest, }, ErrMissingSignTag: { - Code: "AccessDenied", + Code: "AuthorizationHeaderMalformed", Description: "Signature header missing Signature field.", HTTPStatusCode: http.StatusBadRequest, },