fix: validate explicit multipart checksum type

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-08-28 08:57:06 +08:00
parent 229fe2b3c3
commit 7e079ff05c
4 changed files with 86 additions and 6 deletions
+66
View File
@@ -420,8 +420,74 @@ func testAPICompleteMultipartChecksumTypeMismatch(obj ObjectLayer, instanceType,
t.Fatalf("%s: object was created despite a rejected checksum type", instanceType)
}
})
t.Run(test.name+"-type-only", func(t *testing.T) {
objectName := "type-mismatch/type-only-" + test.name
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
typ.String(), test.createdType)
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])}
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS,
map[string]string{xhttp.AmzChecksumType: test.providedType})
if rec.Code != http.StatusBadRequest {
t.Fatalf("%s: checksum type-only mismatch returned %d, want 400", instanceType, rec.Code)
}
apiErr := apiError(t, rec)
if apiErr.Code != "BadDigest" {
t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code)
}
wantMessage := fmt.Sprintf("The checksum type %s does not match the multipart upload checksum type %s.", test.providedType, test.createdType)
if apiErr.Message != wantMessage {
t.Fatalf("%s: expected message %q, got %q", instanceType, wantMessage, apiErr.Message)
}
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
t.Fatalf("%s: object was created despite a rejected checksum type-only assertion", instanceType)
}
})
}
for _, test := range []struct {
name string
providedType string
withChecksum bool
}{
{name: "unknown-type-only", providedType: "NOT_A_TYPE"},
{name: "lowercase-type-only", providedType: "full_object"},
{name: "unknown-with-checksum", providedType: "NOT_A_TYPE", withChecksum: true},
} {
t.Run(test.name, func(t *testing.T) {
objectName := "type-mismatch/invalid-" + test.name
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
typ.String(), xhttp.AmzChecksumTypeComposite)
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])}
headers := map[string]string{xhttp.AmzChecksumType: test.providedType}
if test.withChecksum {
headers[typ.Key()] = mustChecksum(t, typ, full)
}
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, headers)
if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" {
t.Fatalf("%s: invalid checksum type returned %d %s", instanceType, rec.Code, rec.Body.String())
}
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
t.Fatalf("%s: object was created despite an invalid checksum type", instanceType)
}
})
}
t.Run("matching-type-only", func(t *testing.T) {
objectName := "type-mismatch/matching-type-only"
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
typ.String(), xhttp.AmzChecksumTypeComposite)
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])}
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS,
map[string]string{xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite})
if rec.Code != http.StatusOK {
t.Fatalf("%s: matching checksum type-only assertion returned %d %s", instanceType, rec.Code, rec.Body.String())
}
})
t.Run("omitted-type-is-not-composite", func(t *testing.T) {
objectName := "type-mismatch/omitted-type"
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
+11 -3
View File
@@ -1173,9 +1173,9 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
var checksumType hash.ChecksumType
if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType])
expectedType := checksumType | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
if opts.WantChecksum != nil {
providedType := opts.WantChecksum.Type | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
expectedType := checksumType | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
if providedType.Base() != expectedType.Base() {
return oi, InvalidArgument{
Bucket: bucket,
@@ -1183,8 +1183,16 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
Err: fmt.Errorf("checksum algorithm mismatch. got %q expected %q", providedType.String(), expectedType.String()),
}
}
if opts.wantChecksumTypeSet && providedType.ObjType() != expectedType.ObjType() {
return oi, completeMultipartChecksumTypeMismatch(providedType.ObjType(), expectedType.ObjType())
}
if opts.wantChecksumType != "" {
providedObjectType := opts.wantChecksumType
// CRC64NVME is always canonicalized to FULL_OBJECT. Preserve this
// behavior until its exact AWS wire semantics have been probed.
if checksumType.Base().Is(hash.ChecksumCRC64NVME) {
providedObjectType = xhttp.AmzChecksumTypeFullObject
}
if providedObjectType != expectedType.ObjType() {
return oi, completeMultipartChecksumTypeMismatch(opts.wantChecksumType, expectedType.ObjType())
}
}
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
+2 -2
View File
@@ -84,8 +84,8 @@ type ObjectOptions struct {
Expiration ExpirationOptions
LifecycleAuditEvent lcAuditEvent
WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload.
wantChecksumTypeSet bool // x-amz-checksum-type was explicitly set on CompleteMultipartUpload.
WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload.
wantChecksumType string // explicit x-amz-checksum-type value on CompleteMultipartUpload.
WantServerSideChecksumType hash.ChecksumType // if set, we compute a server-side checksum of this type
+7 -1
View File
@@ -469,11 +469,17 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object
}
}
opts.wantChecksumType = r.Header.Get(xhttp.AmzChecksumType)
switch opts.wantChecksumType {
case "", xhttp.AmzChecksumTypeComposite, xhttp.AmzChecksumTypeFullObject:
default:
return opts, hash.ErrInvalidChecksum
}
opts.WantChecksum, err = hash.GetContentChecksum(r.Header)
if err != nil {
return opts, err
}
opts.wantChecksumTypeSet = r.Header.Get(xhttp.AmzChecksumType) != ""
opts.MTime = mtime
opts.UserDefined = make(map[string]string)
// Transfer SSEC key in opts.EncryptFn