Skip to content

Commit 19e4b5e

Browse files
committed
CLDSRV-898: reject Checksum<X> field on a default MPU checksum
1 parent e36d31e commit 19e4b5e

3 files changed

Lines changed: 191 additions & 52 deletions

File tree

lib/api/completeMultipartUpload.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ function validatePerPartChecksums(jsonList, storedParts, mpuSplitter, mpuChecksu
8181

8282
const presentTags = allChecksumXmlTags.filter(tag => part[tag]);
8383

84+
// AWS rejects any per-part Checksum<X> field on a default MPU
85+
// (one created without an explicit ChecksumAlgorithm) with
86+
// InvalidPart — even when the value is correct and even when the
87+
// field matches the implicit default algorithm.
88+
if (mpuChecksum.isDefault && presentTags.length > 0) {
89+
return errorInstances.InvalidPart.customizeDescription(
90+
'One or more of the specified parts could not be found. ' +
91+
'The part may not have been uploaded, or the specified ' +
92+
"entity tag may not match the part's entity tag.",
93+
);
94+
}
95+
8496
for (const tag of presentTags) {
8597
if (tag !== expectedTag) {
8698
const algoLabel = tag.replace(/^Checksum/, '').toLowerCase();

tests/functional/aws-node-sdk/test/object/completeMpuChecksum.js

Lines changed: 121 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -134,53 +134,126 @@ describe('CompleteMultipartUpload final-object checksum', () =>
134134
});
135135
});
136136

137-
it('should return CRC64NVME/FULL_OBJECT on CompleteMPU response when CreateMPU sent no checksum headers', async () => {
138-
const key = `complete-default-${Date.now()}`;
139-
140-
const create = await s3.send(
141-
new CreateMultipartUploadCommand({
142-
Bucket: bucket,
143-
Key: key,
144-
}),
145-
);
146-
147-
const uploadPart = await s3.send(
148-
new UploadPartCommand({
149-
Bucket: bucket,
150-
Key: key,
151-
UploadId: create.UploadId,
152-
PartNumber: 1,
153-
Body: partBody,
154-
}),
155-
);
156-
157-
const complete = await s3.send(
158-
new CompleteMultipartUploadCommand({
159-
Bucket: bucket,
160-
Key: key,
161-
UploadId: create.UploadId,
162-
MultipartUpload: {
163-
Parts: [{ PartNumber: 1, ETag: uploadPart.ETag }],
164-
},
165-
}),
166-
);
167-
168-
assert(
169-
complete.ChecksumCRC64NVME,
170-
`expected ChecksumCRC64NVME for default MPU, got: ${JSON.stringify(complete)}`,
171-
);
172-
assert.strictEqual(complete.ChecksumType, 'FULL_OBJECT');
173-
174-
// Default MPU is FULL_OBJECT — checksum is persisted, so
175-
// HeadObject must return the same value.
176-
const head = await s3.send(
177-
new HeadObjectCommand({
178-
Bucket: bucket,
179-
Key: key,
180-
ChecksumMode: 'ENABLED',
181-
}),
182-
);
183-
assert.strictEqual(head.ChecksumCRC64NVME, complete.ChecksumCRC64NVME);
184-
assert.strictEqual(head.ChecksumType, 'FULL_OBJECT');
137+
it(
138+
'should return CRC64NVME/FULL_OBJECT on CompleteMPU response ' + 'when CreateMPU sent no checksum headers',
139+
async () => {
140+
const key = `complete-default-${Date.now()}`;
141+
142+
const create = await s3.send(
143+
new CreateMultipartUploadCommand({
144+
Bucket: bucket,
145+
Key: key,
146+
}),
147+
);
148+
149+
const uploadPart = await s3.send(
150+
new UploadPartCommand({
151+
Bucket: bucket,
152+
Key: key,
153+
UploadId: create.UploadId,
154+
PartNumber: 1,
155+
Body: partBody,
156+
}),
157+
);
158+
159+
const complete = await s3.send(
160+
new CompleteMultipartUploadCommand({
161+
Bucket: bucket,
162+
Key: key,
163+
UploadId: create.UploadId,
164+
MultipartUpload: {
165+
Parts: [{ PartNumber: 1, ETag: uploadPart.ETag }],
166+
},
167+
}),
168+
);
169+
170+
assert(
171+
complete.ChecksumCRC64NVME,
172+
`expected ChecksumCRC64NVME for default MPU, got: ${JSON.stringify(complete)}`,
173+
);
174+
assert.strictEqual(complete.ChecksumType, 'FULL_OBJECT');
175+
176+
// Default MPU is FULL_OBJECT — checksum is persisted, so
177+
// HeadObject must return the same value.
178+
const head = await s3.send(
179+
new HeadObjectCommand({
180+
Bucket: bucket,
181+
Key: key,
182+
ChecksumMode: 'ENABLED',
183+
}),
184+
);
185+
assert.strictEqual(head.ChecksumCRC64NVME, complete.ChecksumCRC64NVME);
186+
assert.strictEqual(head.ChecksumType, 'FULL_OBJECT');
187+
},
188+
);
189+
190+
// AWS S3 rejects any per-part
191+
// Checksum<X> field on a default MPU (one created without an
192+
// explicit ChecksumAlgorithm) with InvalidPart — even when the
193+
// field matches the implicit CRC64NVME algorithm and value.
194+
describe('default MPU rejects per-part Checksum fields', () => {
195+
async function setupDefaultMpu() {
196+
const key = `complete-default-rejects-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
197+
const create = await s3.send(new CreateMultipartUploadCommand({ Bucket: bucket, Key: key }));
198+
const uploadPart = await s3.send(
199+
new UploadPartCommand({
200+
Bucket: bucket,
201+
Key: key,
202+
UploadId: create.UploadId,
203+
PartNumber: 1,
204+
Body: partBody,
205+
}),
206+
);
207+
return { key, uploadId: create.UploadId, eTag: uploadPart.ETag };
208+
}
209+
210+
async function assertInvalidPart(promise) {
211+
let caught;
212+
try {
213+
await promise;
214+
} catch (err) {
215+
caught = err;
216+
}
217+
assert(caught, 'expected CompleteMPU to reject');
218+
assert.strictEqual(
219+
caught.name,
220+
'InvalidPart',
221+
`expected InvalidPart, got ${caught.name}: ${caught.message}`,
222+
);
223+
}
224+
225+
it('should return InvalidPart when Part includes matching ChecksumCRC64NVME (correct value)', async () => {
226+
const { key, uploadId, eTag } = await setupDefaultMpu();
227+
const crc64 = await algorithms.crc64nvme.digest(partBody);
228+
await assertInvalidPart(
229+
s3.send(
230+
new CompleteMultipartUploadCommand({
231+
Bucket: bucket,
232+
Key: key,
233+
UploadId: uploadId,
234+
MultipartUpload: {
235+
Parts: [{ PartNumber: 1, ETag: eTag, ChecksumCRC64NVME: crc64 }],
236+
},
237+
}),
238+
),
239+
);
240+
});
241+
242+
it('should return InvalidPart when Part includes a non-matching algorithm field', async () => {
243+
const { key, uploadId, eTag } = await setupDefaultMpu();
244+
const crc32 = await algorithms.crc32.digest(partBody);
245+
await assertInvalidPart(
246+
s3.send(
247+
new CompleteMultipartUploadCommand({
248+
Bucket: bucket,
249+
Key: key,
250+
UploadId: uploadId,
251+
MultipartUpload: {
252+
Parts: [{ PartNumber: 1, ETag: eTag, ChecksumCRC32: crc32 }],
253+
},
254+
}),
255+
),
256+
);
257+
});
185258
});
186259
}));

tests/unit/api/multipartUpload.js

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3687,11 +3687,11 @@ describe('initiateMultipartUpload checksum headers', () => {
36873687

36883688
describe('validatePerPartChecksums', () => {
36893689
describe('AWS combination matrix', () => {
3690-
MATRIX.forEach(({ algorithm, type, isDefault }) => {
3691-
const label = `${algorithm}/${type}${isDefault ? ' (default)' : ''}`;
3690+
MATRIX.forEach(({ algorithm, type }) => {
3691+
const label = `${algorithm}/${type}`;
36923692
const tag = TAG_BY_ALGO[algorithm];
36933693
const [d1, d2] = SAMPLE_DIGESTS[algorithm];
3694-
const mpuChecksum = { algorithm, type, isDefault };
3694+
const mpuChecksum = { algorithm, type, isDefault: false };
36953695

36963696
const stored = [makeStoredPart(1, { algorithm, value: d1 }), makeStoredPart(2, { algorithm, value: d2 })];
36973697

@@ -3742,7 +3742,7 @@ describe('validatePerPartChecksums', () => {
37423742
);
37433743
});
37443744

3745-
const requiresPerPart = type === 'COMPOSITE' && !isDefault;
3745+
const requiresPerPart = type === 'COMPOSITE';
37463746
const missingLabel = requiresPerPart
37473747
? 'should return InvalidRequest when a part is missing its checksum'
37483748
: 'should accept a parts list missing per-part checksums';
@@ -3764,6 +3764,60 @@ describe('validatePerPartChecksums', () => {
37643764
});
37653765
});
37663766

3767+
describe('default MPU (isDefault=true)', () => {
3768+
// AWS S3 rejects any per-part
3769+
// Checksum<X> field on a default MPU with InvalidPart — even when
3770+
// the field matches the implicit CRC64NVME algorithm and the value
3771+
// is the same one the part was stored with.
3772+
const mpuChecksum = { algorithm: 'crc64nvme', type: 'FULL_OBJECT', isDefault: true };
3773+
const [d1, d2] = SAMPLE_DIGESTS.crc64nvme;
3774+
const stored = [
3775+
makeStoredPart(1, { algorithm: 'crc64nvme', value: d1 }),
3776+
makeStoredPart(2, { algorithm: 'crc64nvme', value: d2 }),
3777+
];
3778+
const invalidPartMessage =
3779+
'One or more of the specified parts could not be ' +
3780+
'found. The part may not have been uploaded, or ' +
3781+
'the specified entity tag may not match the ' +
3782+
"part's entity tag.";
3783+
3784+
it('should accept when no parts include a checksum field', () => {
3785+
const jsonList = { Part: [makeJsonPart(1, 'etag1'), makeJsonPart(2, 'etag2')] };
3786+
const err = validatePerPartChecksums(jsonList, stored, splitter, mpuChecksum);
3787+
assert.strictEqual(err, null);
3788+
});
3789+
3790+
it('should return InvalidPart when a part includes the matching field (correct value)', () => {
3791+
const jsonList = {
3792+
Part: [makeJsonPart(1, 'etag1', { ChecksumCRC64NVME: d1 }), makeJsonPart(2, 'etag2')],
3793+
};
3794+
const err = validatePerPartChecksums(jsonList, stored, splitter, mpuChecksum);
3795+
assert(err);
3796+
assert.strictEqual(err.is.InvalidPart, true);
3797+
assert.strictEqual(err.description, invalidPartMessage);
3798+
});
3799+
3800+
it('should return InvalidPart when a part includes the matching field (wrong value)', () => {
3801+
const jsonList = {
3802+
Part: [makeJsonPart(1, 'etag1', { ChecksumCRC64NVME: d2 }), makeJsonPart(2, 'etag2')],
3803+
};
3804+
const err = validatePerPartChecksums(jsonList, stored, splitter, mpuChecksum);
3805+
assert(err);
3806+
assert.strictEqual(err.is.InvalidPart, true);
3807+
assert.strictEqual(err.description, invalidPartMessage);
3808+
});
3809+
3810+
it('should return InvalidPart when a part includes a non-matching algorithm field', () => {
3811+
const jsonList = {
3812+
Part: [makeJsonPart(1, 'etag1', { ChecksumCRC32: SAMPLE_DIGESTS.crc32[0] }), makeJsonPart(2, 'etag2')],
3813+
};
3814+
const err = validatePerPartChecksums(jsonList, stored, splitter, mpuChecksum);
3815+
assert(err);
3816+
assert.strictEqual(err.is.InvalidPart, true);
3817+
assert.strictEqual(err.description, invalidPartMessage);
3818+
});
3819+
});
3820+
37673821
describe('legacy MPU (no algorithm configured)', () => {
37683822
// Pre-feature MPUs have storedMetadata.checksumAlgorithm === undefined.
37693823
// Pre-PR CompleteMPU silently ignored any per-part Checksum<X> body

0 commit comments

Comments
 (0)