From 12fb5a65948e17bb650cb7a40e45f7e7365173d3 Mon Sep 17 00:00:00 2001 From: niksis02 Date: Mon, 15 Jun 2026 18:52:06 +0400 Subject: [PATCH] fix: reject empty Content-MD5 on PUT operations Fixes #2179 Treat a present but empty `Content-MD5` header as an invalid digest instead of handling it as if the header were absent. This makes PUT operations return `InvalidDigest` for empty `Content-MD5` values while preserving existing behavior for missing headers. --- s3api/middlewares/checksum.go | 10 +++++-- tests/integration/PutObject.go | 50 ++++++++++++++++++++++++++++++++ tests/integration/group-tests.go | 4 +-- tests/integration/sigv4_auth.go | 39 ------------------------- 4 files changed, 60 insertions(+), 43 deletions(-) diff --git a/s3api/middlewares/checksum.go b/s3api/middlewares/checksum.go index c1955284..62d68fdc 100644 --- a/s3api/middlewares/checksum.go +++ b/s3api/middlewares/checksum.go @@ -33,13 +33,19 @@ import ( // the x-amz-checksum-* headers are explicitly processed by the backend. func VerifyChecksums(streamBody bool, requireBody bool, requireChecksum bool) fiber.Handler { return func(ctx fiber.Ctx) error { - md5sum := ctx.Get("Content-Md5") + md5Header := ctx.Request().Header.Peek("Content-Md5") + hasMD5Header := md5Header != nil + md5sum := string(md5Header) + + if hasMD5Header && md5sum == "" { + return s3err.GetInvalidDigestErr(md5sum) + } if streamBody { // for large data actions(PutObject, UploadPart) // only stack the md5 reader,as x-amz-checksum-* // calculation is explicitly handled in back-end - if md5sum == "" { + if !hasMD5Header { return nil } diff --git a/tests/integration/PutObject.go b/tests/integration/PutObject.go index f27db548..01b62cf0 100644 --- a/tests/integration/PutObject.go +++ b/tests/integration/PutObject.go @@ -17,6 +17,8 @@ package integration import ( "bytes" "context" + "crypto/md5" + "encoding/base64" "fmt" "net/http" "strings" @@ -642,6 +644,54 @@ func PutObject_invalid_website_redirect_location(s *S3Conf) error { }) } +func PutObject_md5(s *S3Conf) error { + testName := "PutObject_md5" + return actionHandler(s, testName, func(_ *s3.Client, bucket string) error { + body := []byte("dummy data") + sum := md5.Sum(body) + contentMd5 := base64.StdEncoding.EncodeToString(sum[:]) + + for i, test := range []struct { + name string + md5 string + err s3err.S3Error + }{ + {"empty_md5", "", s3err.GetInvalidDigestErr("")}, + {"invalid_md5", "invalid_md5", s3err.GetInvalidDigestErr("invalid_md5")}, + // valid base64, but invalid md5 + {"invalid_md5_length", "aGVsbCBzLGRham5mamFuc2Y=", s3err.GetInvalidDigestErr("aGVsbCBzLGRham5mamFuc2Y=")}, + // valid md5, but incorrect + {"incorrect_md5", "XrY7u+Ae7tCTyyK7j1rNww==", s3err.GetBadDigestErr(contentMd5, base64ToHexString("XrY7u+Ae7tCTyyK7j1rNww=="))}, + {"success", contentMd5, nil}, + } { + req, err := createSignedReq(http.MethodPut, s.endpoint, fmt.Sprintf("%s/obj-%d", bucket, i), s.awsID, s.awsSecret, "s3", s.awsRegion, "", body, time.Now(), map[string]string{ + "Content-Md5": test.md5, + }) + if err != nil { + return err + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return err + } + + if test.err == nil { + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("test %q failed: expected response status code to be %v, instead got %v", test.name, http.StatusOK, resp.StatusCode) + } + continue + } + + if err := checkHTTPResponseApiErr(resp, test.err); err != nil { + return fmt.Errorf("test %q failed: %w", test.name, err) + } + } + + return nil + }) +} + func PutObject_checksum_algorithm_and_header_mismatch(s *S3Conf) error { testName := "PutObject_checksum_algorithm_and_header_mismatch" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 9dc93c73..e4225beb 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -36,7 +36,6 @@ func TestAuthentication(ts *TestState) { ts.Run(Authentication_date_mismatch) ts.Run(Authentication_incorrect_payload_hash) ts.Run(Authentication_invalid_sha256_payload_hash) - ts.Run(Authentication_md5) ts.Run(Authentication_unsigned_required_header) ts.Run(Authentication_unsigned_non_required_header) ts.Run(Authentication_signature_error_incorrect_secret_key) @@ -179,6 +178,7 @@ func TestPutObject(ts *TestState) { ts.Run(PutObject_invalid_retain_until_date) ts.Run(PutObject_conditional_writes) ts.Run(PutObject_should_combine_metadata) + ts.Run(PutObject_md5) //TODO: remove the condition after implementing checksums in azure if !ts.conf.azureTests { ts.Run(PutObject_checksum_algorithm_and_header_mismatch) @@ -1362,7 +1362,6 @@ func GetIntTests() IntTests { "Authentication_date_mismatch": Authentication_date_mismatch, "Authentication_incorrect_payload_hash": Authentication_incorrect_payload_hash, "Authentication_invalid_sha256_payload_hash": Authentication_invalid_sha256_payload_hash, - "Authentication_md5": Authentication_md5, "Authentication_unsigned_required_header": Authentication_unsigned_required_header, "Authentication_unsigned_non_required_header": Authentication_unsigned_non_required_header, "Authentication_signature_error_incorrect_secret_key": Authentication_signature_error_incorrect_secret_key, @@ -1402,6 +1401,7 @@ func GetIntTests() IntTests { "PutObject_invalid_retain_until_date": PutObject_invalid_retain_until_date, "PutObject_conditional_writes": PutObject_conditional_writes, "PutObject_should_combine_metadata": PutObject_should_combine_metadata, + "PutObject_md5": PutObject_md5, "PutObject_long_metadata": PutObject_long_metadata, "PutObject_with_metadata": PutObject_with_metadata, "PutObject_invalid_website_redirect_location": PutObject_invalid_website_redirect_location, diff --git a/tests/integration/sigv4_auth.go b/tests/integration/sigv4_auth.go index b2650d68..afdedfb4 100644 --- a/tests/integration/sigv4_auth.go +++ b/tests/integration/sigv4_auth.go @@ -15,8 +15,6 @@ package integration import ( - "crypto/md5" - "encoding/base64" "encoding/xml" "fmt" "io" @@ -500,43 +498,6 @@ func Authentication_incorrect_payload_hash(s *S3Conf) error { }) } -func Authentication_md5(s *S3Conf) error { - testName := "Authentication_md5" - return actionHandler(s, testName, func(_ *s3.Client, bucket string) error { - sum := md5.Sum(nil) - emptyMd5 := base64.StdEncoding.EncodeToString(sum[:]) - - for i, test := range []struct { - md5 string - err s3err.S3Error - }{ - {"invalid_md5", s3err.GetInvalidDigestErr("invalid_md5")}, - // valid base64, but invalid md5 - {"aGVsbCBzLGRham5mamFuc2Y=", s3err.GetInvalidDigestErr("aGVsbCBzLGRham5mamFuc2Y=")}, - // valid md5, but incorrect - {"XrY7u+Ae7tCTyyK7j1rNww==", s3err.GetBadDigestErr(emptyMd5, base64ToHexString("XrY7u+Ae7tCTyyK7j1rNww=="))}, - } { - req, err := createSignedReq(http.MethodPut, s.endpoint, fmt.Sprintf("%s/obj", bucket), s.awsID, s.awsSecret, "s3", s.awsRegion, "", nil, time.Now(), map[string]string{ - "Content-Md5": test.md5, - }) - if err != nil { - return err - } - - resp, err := s.httpClient.Do(req) - if err != nil { - return err - } - - if err := checkHTTPResponseApiErr(resp, test.err); err != nil { - return fmt.Errorf("test %v failed: %w", i+1, err) - } - } - - return nil - }) -} - func Authentication_unsigned_required_header(s *S3Conf) error { testName := "Authentication_unsigned_required_header" return actionHandler(s, testName, func(_ *s3.Client, bucket string) error {