CLDSRV-932: validate x-amz-content-sha256#6208
Conversation
Hello leif-scality,My role is to assist you with the merge of this Available options
Available commands
Status report is not available. |
Incorrect fix versionThe
Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:
Please check the |
❌ 2 Tests Failed:
View the full list of 2 ❄️ flaky test(s)
To view more test analytics, go to the Test Analytics Dashboard |
969e37f to
ffbc547
Compare
9070485 to
9aafbf6
Compare
fredmnl
left a comment
There was a problem hiding this comment.
Overall, I'm approving. Unless you have a strong reason not to (if the AWS implementation/spec doesn't comply in this case), please include the zero-byte test case.
| }); | ||
| }); | ||
|
|
||
| makeMismatchTests(() => `http://${host}:${port}/${bucket}/${objectKey}`); |
There was a problem hiding this comment.
const emptyBody = Buffer.alloc(0);
const emptySha256Hex = crypto.createHash('sha256').update(emptyBody).digest('hex');
describe('with an empty body (zero-byte path)', () => {
makeMismatchTests(() => `http://${host}:${port}/${bucket}/${objectKey}`, emptyBody, emptySha256Hex);
});
Claude suggested this test for an zero-byte object (and is saying that it will fail)
There was a problem hiding this comment.
fixed PutObject zero byte case. Also added multiple zero-byte tests for the other endpoints
|
ping |
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
|
|
/approve |
Build failedThe build for commit did not succeed in branch bugfix/CLDSRV-932-check-x-amz-content-sha256 The following options are set: approve |
ConflictThere is a conflict between your branch Please resolve the conflict on the feature branch ( git fetch && \
git checkout origin/bugfix/CLDSRV-932-check-x-amz-content-sha256 && \
git merge origin/development/9.4Resolve merge conflicts and commit git push origin HEAD:bugfix/CLDSRV-932-check-x-amz-content-sha256The following options are set: approve |
…ject, UploadPart)
8d88ff2 to
37fa6dc
Compare
| return Promise.resolve(algorithms[algorithm].digest(Buffer.alloc(0))).then( | ||
| value => { | ||
| if (expected !== undefined && expected !== value) { | ||
| return callback(errorInstances.BadDigest.customizeDescription( | ||
| `The ${algorithm.toUpperCase()} you specified did not match the calculated checksum.` | ||
| )); | ||
| return callback( | ||
| errorInstances.BadDigest.customizeDescription( | ||
| `The ${algorithm.toUpperCase()} you specified did not match the calculated checksum.`, | ||
| ), | ||
| ); | ||
| } | ||
| // eslint-disable-next-line no-param-reassign | ||
| metadataStoreParams.checksum = { algorithm, value, type: 'FULL_OBJECT' }; | ||
| return callback(null); | ||
| }, err => callback(err)); | ||
| }, | ||
| err => callback(err), | ||
| ); |
| return next(arsenalErrorFromChecksumError(headerChecksum)); | ||
| } | ||
| const checksums = { | ||
| primary: headerChecksum || defaultChecksumData, | ||
| secondary: null, | ||
| }; | ||
| return dataStore(objectKeyContext, cipherBundle, request, size, | ||
| streamingV4Params, backendInfo, checksums, log, next); | ||
| }, | ||
| function processDataResult(dataGetInfo, calculatedHash, checksum, next) { | ||
| if (dataGetInfo === null || dataGetInfo === undefined) { | ||
| return next(null, null); | ||
| } | ||
| // So that data retrieval information for MPU's and | ||
| // regular puts are stored in the same data structure, | ||
| // place the retrieval info here into a single element array | ||
| const { key, dataStoreName, dataStoreType, dataStoreETag, | ||
| dataStoreVersionId } = dataGetInfo; | ||
| const prefixedDataStoreETag = dataStoreETag | ||
| ? `1:${dataStoreETag}` | ||
| : `1:${calculatedHash}`; | ||
| const dataGetInfoArr = [{ key, size, start: 0, dataStoreName, | ||
| dataStoreType, dataStoreETag: prefixedDataStoreETag, | ||
| dataStoreVersionId | ||
| }]; | ||
| if (cipherBundle) { | ||
| dataGetInfoArr[0].cryptoScheme = cipherBundle.cryptoScheme; | ||
| dataGetInfoArr[0].cipheredDataKey = | ||
| cipherBundle.cipheredDataKey; | ||
| } | ||
| if (mdOnlyHeader === 'true') { | ||
| metadataStoreParams.size = mdOnlySize; | ||
| dataGetInfoArr[0].size = mdOnlySize; | ||
| } | ||
| metadataStoreParams.contentMD5 = calculatedHash; | ||
| if (checksum) { | ||
| // eslint-disable-next-line no-param-reassign | ||
| checksum.type = 'FULL_OBJECT'; | ||
| metadataStoreParams.checksum = checksum; | ||
| } | ||
| return next(null, dataGetInfoArr); | ||
| }, | ||
| function getVersioningInfo(infoArr, next) { | ||
| // if x-scal-s3-version-id header is specified, we overwrite the object/version metadata. | ||
| if (isPutVersion) { | ||
| const options = overwritingVersioning(objMD, metadataStoreParams); | ||
| return process.nextTick(() => next(null, options, infoArr)); | ||
| } | ||
| const headerChecksum = getChecksumDataFromHeaders(request.headers); | ||
| if (headerChecksum && headerChecksum.error) { | ||
| return next(arsenalErrorFromChecksumError(headerChecksum)); | ||
| } | ||
| const checksums = { | ||
| primary: headerChecksum || defaultChecksumData, | ||
| secondary: null, | ||
| }; | ||
| return dataStore( | ||
| objectKeyContext, | ||
| cipherBundle, | ||
| request, | ||
| size, | ||
| streamingV4Params, | ||
| backendInfo, | ||
| checksums, | ||
| log, | ||
| next, | ||
| ); | ||
| }, | ||
| function processDataResult(dataGetInfo, calculatedHash, checksum, next) { | ||
| if (dataGetInfo === null || dataGetInfo === undefined) { | ||
| return next(null, null); | ||
| } | ||
| // So that data retrieval information for MPU's and | ||
| // regular puts are stored in the same data structure, | ||
| // place the retrieval info here into a single element array | ||
| const { key, dataStoreName, dataStoreType, dataStoreETag, dataStoreVersionId } = dataGetInfo; | ||
| const prefixedDataStoreETag = dataStoreETag ? `1:${dataStoreETag}` : `1:${calculatedHash}`; | ||
| const dataGetInfoArr = [ | ||
| { | ||
| key, | ||
| size, | ||
| start: 0, | ||
| dataStoreName, | ||
| dataStoreType, | ||
| dataStoreETag: prefixedDataStoreETag, | ||
| dataStoreVersionId, | ||
| }, | ||
| ]; | ||
| if (cipherBundle) { | ||
| dataGetInfoArr[0].cryptoScheme = cipherBundle.cryptoScheme; | ||
| dataGetInfoArr[0].cipheredDataKey = cipherBundle.cipheredDataKey; | ||
| } | ||
| if (mdOnlyHeader === 'true') { | ||
| metadataStoreParams.size = mdOnlySize; | ||
| dataGetInfoArr[0].size = mdOnlySize; | ||
| } | ||
| metadataStoreParams.contentMD5 = calculatedHash; | ||
| if (checksum) { | ||
| // eslint-disable-next-line no-param-reassign | ||
| checksum.type = 'FULL_OBJECT'; | ||
| metadataStoreParams.checksum = checksum; | ||
| } | ||
| return next(null, dataGetInfoArr); | ||
| }, | ||
| function getVersioningInfo(infoArr, next) { | ||
| // if x-scal-s3-version-id header is specified, we overwrite the object/version metadata. | ||
| if (isPutVersion) { | ||
| const options = overwritingVersioning(objMD, metadataStoreParams); | ||
| return process.nextTick(() => next(null, options, infoArr)); | ||
| } | ||
|
|
||
| if (!bucketMD.isVersioningEnabled() && objMD?.archive?.archiveInfo) { | ||
| // Ensure we trigger a "delete" event in the oplog for the previously archived object | ||
| metadataStoreParams.needOplogUpdate = 's3:ReplaceArchivedObject'; | ||
| } | ||
| if (!bucketMD.isVersioningEnabled() && objMD?.archive?.archiveInfo) { | ||
| // Ensure we trigger a "delete" event in the oplog for the previously archived object | ||
| metadataStoreParams.needOplogUpdate = 's3:ReplaceArchivedObject'; | ||
| } | ||
|
|
||
| return versioningPreprocessing(bucketName, bucketMD, | ||
| metadataStoreParams.objectKey, objMD, log, (err, options) => { | ||
| if (err) { | ||
| // TODO: check AWS error when user requested a specific | ||
| // version before any versions have been put | ||
| const logLvl = err.is.BadRequest ? | ||
| 'debug' : 'error'; | ||
| log[logLvl]('error getting versioning info', { | ||
| error: err, | ||
| method: 'versioningPreprocessing', | ||
| }); | ||
| } | ||
| return versioningPreprocessing( | ||
| bucketName, | ||
| bucketMD, | ||
| metadataStoreParams.objectKey, | ||
| objMD, | ||
| log, |
| (err, options) => { | ||
| if (err) { | ||
| // TODO: check AWS error when user requested a specific | ||
| // version before any versions have been put | ||
| const logLvl = err.is.BadRequest ? 'debug' : 'error'; | ||
| log[logLvl]('error getting versioning info', { | ||
| error: err, | ||
| method: 'versioningPreprocessing', | ||
| }); | ||
| } | ||
|
|
||
| const location = infoArr?.[0]?.dataStoreName; | ||
| if (location === bucketMD.getLocationConstraint() && bucketMD.isIngestionBucket()) { | ||
| // If the object is being written to the "ingested" storage location, keep the same | ||
| // versionId for consistency and to avoid creating an extra version when it gets | ||
| // ingested | ||
| const backendVersionId = decodeVID(infoArr[0].dataStoreVersionId); | ||
| if (!(backendVersionId instanceof Error)) { | ||
| options.versionId = backendVersionId; // eslint-disable-line no-param-reassign | ||
| const location = infoArr?.[0]?.dataStoreName; | ||
| if (location === bucketMD.getLocationConstraint() && bucketMD.isIngestionBucket()) { | ||
| // If the object is being written to the "ingested" storage location, keep the same | ||
| // versionId for consistency and to avoid creating an extra version when it gets | ||
| // ingested | ||
| const backendVersionId = decodeVID(infoArr[0].dataStoreVersionId); | ||
| if (!(backendVersionId instanceof Error)) { | ||
| options.versionId = backendVersionId; // eslint-disable-line no-param-reassign | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return next(err, options, infoArr); | ||
| }); | ||
| }, | ||
| function storeMDAndDeleteData(options, infoArr, next) { | ||
| metadataStoreParams.versionId = options.versionId; |
| function handleTransientOrDeleteBuckets(next) { | ||
| if (bucket.hasTransientFlag() || bucket.hasDeletedFlag()) { | ||
| return cleanUpBucket(bucket, canonicalID, log, next); | ||
| } | ||
| return next(null, sseConfig); | ||
| } | ||
| ); | ||
| }, | ||
| function createCipherBundle(serverSideEncryptionConfig, next) { | ||
| if (serverSideEncryptionConfig) { | ||
| return kms.createCipherBundle( | ||
| serverSideEncryptionConfig, log, (err, cipherBundle) => { | ||
| return next(); | ||
| }, |
| function getSSEConfig(next) { | ||
| return getObjectSSEConfiguration(headers, bucket, log, (err, sseConfig) => { | ||
| if (err) { | ||
| return next(err); | ||
| log.error('error getting server side encryption config', { err }); | ||
| return next(invalidSSEError); | ||
| } | ||
| setSSEHeaders(responseHeaders, | ||
| cipherBundle.algorithm, | ||
| cipherBundle.configuredMasterKeyId || cipherBundle.masterKeyId); | ||
| return next(null, cipherBundle); | ||
| return next(null, sseConfig); | ||
| }); | ||
| } | ||
| return next(null, null); | ||
| }, | ||
| function objectCreateAndStore(cipherBundle, next) { | ||
| const objectLockValidationError | ||
| = validateHeaders(bucket, headers, log); | ||
| if (objectLockValidationError) { | ||
| return next(objectLockValidationError); | ||
| } | ||
| writeContinue(request, request._response); | ||
| return createAndStoreObject(bucketName, | ||
| bucket, objectKey, objMD, authInfo, canonicalID, cipherBundle, | ||
| request, false, streamingV4Params, overheadField, log, 's3:ObjectCreated:Put', next); | ||
| }, | ||
| ], (err, storingResult) => { | ||
| if (err) { | ||
| monitoring.promMetrics('PUT', bucketName, err.code, | ||
| 'putObject'); | ||
| return callback(err, responseHeaders); | ||
| } | ||
| // ingestSize assumes that these custom headers indicate | ||
| // an ingestion PUT which is a metadata only operation. | ||
| // Since these headers can be modified client side, they | ||
| // should be used with caution if needed for precise | ||
| // metrics. | ||
| const ingestSize = (request.headers['x-amz-meta-mdonly'] | ||
| && !Number.isNaN(request.headers['x-amz-meta-size'])) | ||
| ? Number.parseInt(request.headers['x-amz-meta-size'], 10) : null; | ||
| const newByteLength = parsedContentLength; | ||
| }, |
| function createCipherBundle(serverSideEncryptionConfig, next) { | ||
| if (serverSideEncryptionConfig) { | ||
| return kms.createCipherBundle(serverSideEncryptionConfig, log, (err, cipherBundle) => { | ||
| if (err) { | ||
| return next(err); | ||
| } | ||
| setSSEHeaders( | ||
| responseHeaders, | ||
| cipherBundle.algorithm, | ||
| cipherBundle.configuredMasterKeyId || cipherBundle.masterKeyId, | ||
| ); | ||
| return next(null, cipherBundle); | ||
| }); | ||
| } | ||
| return next(null, null); | ||
| }, |
| function objectCreateAndStore(cipherBundle, next) { | ||
| const objectLockValidationError = validateHeaders(bucket, headers, log); | ||
| if (objectLockValidationError) { | ||
| return next(objectLockValidationError); | ||
| } | ||
| writeContinue(request, request._response); | ||
| return createAndStoreObject( | ||
| bucketName, | ||
| bucket, | ||
| objectKey, | ||
| objMD, | ||
| authInfo, | ||
| canonicalID, | ||
| cipherBundle, | ||
| request, | ||
| false, | ||
| streamingV4Params, | ||
| overheadField, | ||
| log, | ||
| 's3:ObjectCreated:Put', | ||
| next, | ||
| ); | ||
| }, |
Build failedThe build for commit did not succeed in branch bugfix/CLDSRV-932-check-x-amz-content-sha256 The following options are set: approve |
|
I have successfully merged the changeset of this pull request
The following branches have NOT changed:
This pull request did not target the following hotfix branch(es) so they
Please check the status of the associated issue CLDSRV-932. Goodbye leif-scality. The following options are set: approve |
Reject a
SigV4request if thesha256of the body does not match thex-amz-content-sha256header