From d28885d0e51b71d7fbb76af7977917db067158dc Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Sat, 29 Aug 2026 17:23:41 +0800 Subject: [PATCH] fix: reject composite CRC64NVME checksums Return InvalidArgument for CRC64NVME with COMPOSITE at multipart initiation and PutObject instead of silently canonicalizing the request to FULL_OBJECT. Signed-off-by: Feng Ruohang --- cmd/erasure-multipart-fullobject_test.go | 20 ++++---- cmd/object-crc64-composite_test.go | 61 ++++++++++++++++++++++++ internal/hash/checksum.go | 8 +++- internal/hash/checksum_test.go | 3 ++ 4 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 cmd/object-crc64-composite_test.go diff --git a/cmd/erasure-multipart-fullobject_test.go b/cmd/erasure-multipart-fullobject_test.go index 0bcb14c4f..4b7021522 100644 --- a/cmd/erasure-multipart-fullobject_test.go +++ b/cmd/erasure-multipart-fullobject_test.go @@ -450,19 +450,21 @@ func testAPICompleteMultipartChecksumTypeMismatch(obj ObjectLayer, instanceType, } }) - t.Run("crc64nvme-composite-remains-canonicalized", func(t *testing.T) { + t.Run("crc64nvme-composite-is-rejected", func(t *testing.T) { crc64Type := hash.ChecksumCRC64NVME objectName := "type-mismatch/crc64nvme-composite" - uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, - crc64Type.String(), xhttp.AmzChecksumTypeComposite) - etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, crc64Type, partData) - rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, nil, - map[string]string{ - crc64Type.Key(): mustChecksum(t, crc64Type, full), + req, err := newTestSignedRequestV4(http.MethodPost, getNewMultipartURL("", bucketName, objectName), + 0, nil, credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzChecksumAlgo: crc64Type.String(), xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, }) - if rec.Code != http.StatusOK { - t.Fatalf("%s: CRC64NVME canonicalization changed: %d %s", instanceType, rec.Code, rec.Body.String()) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: CRC64NVME/COMPOSITE returned %d %s", instanceType, rec.Code, rec.Body.String()) } }) } diff --git a/cmd/object-crc64-composite_test.go b/cmd/object-crc64-composite_test.go new file mode 100644 index 000000000..6db038062 --- /dev/null +++ b/cmd/object-crc64-composite_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2015-2026 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/hash" + xhttp "github.com/minio/minio/internal/http" +) + +func TestAPIPutObjectRejectsCRC64Composite(t *testing.T) { + defer DetectTestLeak(t)() + ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{ + t: t, + objAPITest: testAPIPutObjectRejectsCRC64Composite, + endpoints: []string{"PutObject"}, + }) +} + +func testAPIPutObjectRejectsCRC64Composite(obj ObjectLayer, instanceType, bucketName string, + apiRouter http.Handler, credentials auth.Credentials, t *testing.T, +) { + data := []byte("crc64-composite") + object := "checksums/crc64-composite" + req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, object), + int64(len(data)), bytes.NewReader(data), credentials.AccessKey, credentials.SecretKey, map[string]string{ + xhttp.AmzChecksumCRC64NVME: mustChecksum(t, hash.ChecksumCRC64NVME, data), + xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite, + }) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + apiRouter.ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" { + t.Fatalf("%s: CRC64NVME/COMPOSITE PutObject returned %d %s", instanceType, rec.Code, rec.Body.String()) + } + if _, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{}); !isErrObjectNotFound(err) { + t.Fatalf("%s: rejected PutObject stored an object: %v", instanceType, err) + } +} diff --git a/internal/hash/checksum.go b/internal/hash/checksum.go index 32d106d90..5da17e1b6 100644 --- a/internal/hash/checksum.go +++ b/internal/hash/checksum.go @@ -157,7 +157,6 @@ func ChecksumStringToType(alg string) ChecksumType { case "SHA256": return ChecksumSHA256 case "CRC64NVME": - // AWS seems to ignore full value, and just assume it. return ChecksumCRC64NVME case "": return ChecksumNone @@ -192,7 +191,9 @@ func NewChecksumType(alg, objType string) ChecksumType { } return ChecksumSHA256 case "CRC64NVME": - // AWS seems to ignore full value, and just assume it. + if objType == xhttp.AmzChecksumTypeComposite { + return ChecksumInvalid + } return ChecksumCRC64NVME case "": if full != 0 { @@ -781,5 +782,8 @@ func getContentChecksum(h http.Header) (t ChecksumType, s string) { for _, t := range BaseChecksumTypes { checkType(t) } + if t.Base().Is(ChecksumCRC64NVME) && h.Get(xhttp.AmzChecksumType) == xhttp.AmzChecksumTypeComposite { + return ChecksumInvalid, "" + } return t, s } diff --git a/internal/hash/checksum_test.go b/internal/hash/checksum_test.go index 9ea81c967..74631a329 100644 --- a/internal/hash/checksum_test.go +++ b/internal/hash/checksum_test.go @@ -67,6 +67,9 @@ func TestGetContentChecksumRejectsUnsupportedHeaders(t *testing.T) { // TestChecksumAddToHeader tests that adding and retrieving a checksum on a header works func TestChecksumAddToHeader(t *testing.T) { + if got := NewChecksumType("CRC64NVME", xhttp.AmzChecksumTypeComposite); !got.Is(ChecksumInvalid) { + t.Fatalf("CRC64NVME/COMPOSITE = %s, want invalid", got.StringFull()) + } tests := []struct { name string checksum ChecksumType