Skip to content

Commit dd591e5

Browse files
committed
CLDSRV-1021: Compare s3:VersionId with the copy source version ID as sent
- Use the copy source version ID as sent by the client, for both the bucket policy and the authorization request, instead of the internal version ID - Return InvalidArgument when the copy source versionId is repeated, so an array never reaches the bucket policy conditions
1 parent f658165 commit dd591e5

7 files changed

Lines changed: 27 additions & 12 deletions

File tree

‎lib/api/api.js‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ function callApiHandler(apiMethod, apiHandler, request, response, log, callback)
258258
return apiHandler(request, log, callback);
259259
}
260260

261-
const { sourceBucket, sourceObject, sourceVersionId, parsingError } = parseCopySource(
261+
const { sourceBucket, sourceObject, sourceVersionId, copySourceVersionId, parsingError } = parseCopySource(
262262
apiMethod,
263263
request.headers['x-amz-copy-source'],
264264
);
@@ -277,7 +277,7 @@ function callApiHandler(apiMethod, apiHandler, request, response, log, callback)
277277
return process.nextTick(callback, httpHeadersSizeError);
278278
}
279279

280-
const requestContexts = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId);
280+
const requestContexts = prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, copySourceVersionId);
281281

282282
// Extract all the _apiMethods and store them in an array
283283
const apiMethods = requestContexts ? requestContexts.map(context => context._apiMethod) : [];
@@ -441,6 +441,9 @@ function callApiHandler(apiMethod, apiHandler, request, response, log, callback)
441441
return apiHandler(userInfo, request, streamingV4Params, log, methodCallback, authorizationResults);
442442
}
443443
if (apiMethod === 'objectCopy' || apiMethod === 'objectPutCopyPart') {
444+
// The bucket policy is evaluated against the version ID as
445+
// sent, the decoded one is for the metadata lookup
446+
request.copySourceVersionId = copySourceVersionId;
444447
return apiHandler(userInfo, request, sourceBucket, sourceObject, sourceVersionId, log, methodCallback);
445448
}
446449
if (apiMethod === 'objectGet') {

‎lib/api/apiUtils/authorization/permissionChecks.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,16 +486,18 @@ function checkBucketPolicy(
486486
// differs from the one the request targets (e.g. the source of a copy)
487487
let bucketName = request?.bucketName;
488488
let objectKey = request?.objectKey;
489+
let query = request?.query;
489490
if (policyTarget) {
490491
bucketName = policyTarget.bucketName;
491492
objectKey = policyTarget.objectKey;
493+
query = Object.assign({}, query, { versionId: policyTarget.versionId });
492494
}
493495
const ip = request ? requestUtils.getClientIp(request, config) : undefined;
494496
const isSecure = request ? requestUtils.getHttpProtocolSecurity(request, config) : undefined;
495497
const requestContext = request
496498
? new RequestContext(
497499
request.headers,
498-
request.query,
500+
query,
499501
bucketName,
500502
objectKey,
501503
ip,

‎lib/api/apiUtils/authorization/prepareRequestContexts.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ function isHeaderAcl(headers) {
3333
* @param {object} request - request object
3434
* @param {string} sourceBucket - name of sourceBucket if copy request
3535
* @param {string} sourceObject - name of sourceObject if copy request
36-
* @param {string} sourceVersionId - value of sourceVersionId if copy request
36+
* @param {string} sourceVersionId - version ID of the copy source as sent by
37+
* the client, if copy request
3738
* @return {RequestContext []} array of requestContexts
3839
*/
3940
function prepareRequestContexts(apiMethod, request, sourceBucket, sourceObject, sourceVersionId) {

‎lib/api/apiUtils/object/parseCopySource.js‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const { decodeVersionId } = require('./versioning');
77
/** parseCopySource - parse objectCopy or objectPutCopyPart copy source header
88
* @param {string} apiMethod - api method
99
* @param {string} copySourceHeader - 'x-amz-copy-source' request header
10-
* @return {object} - sourceBucket, sourceObject, sourceVersionId, parsingError
10+
* @return {object} - sourceBucket, sourceObject, sourceVersionId (decoded),
11+
* copySourceVersionId (as sent by the client), parsingError
1112
*/
1213
function parseCopySource(apiMethod, copySourceHeader) {
1314
if (apiMethod !== 'objectCopy' && apiMethod !== 'objectPutCopyPart') {
@@ -26,14 +27,21 @@ function parseCopySource(apiMethod, copySourceHeader) {
2627
// Pull the source bucket and source object separated by /
2728
const sourceBucket = source.slice(0, slashSeparator);
2829
const sourceObject = source.slice(slashSeparator + 1);
29-
const sourceVersionId =
30-
decodeVersionId(query ? querystring.parse(query) : undefined);
30+
const sourceQuery = query ? querystring.parse(query) : undefined;
31+
// A repeated versionId is parsed as an array
32+
if (sourceQuery?.versionId !== undefined && typeof sourceQuery.versionId !== 'string') {
33+
return { parsingError: errors.InvalidArgument };
34+
}
35+
const sourceVersionId = decodeVersionId(sourceQuery);
3136
if (sourceVersionId instanceof Error) {
3237
const err = sourceVersionId;
3338
return { parsingError: err };
3439
}
40+
// Authorization compares s3:VersionId with the version ID as the client
41+
// sent it, like for a direct GetObject
42+
const copySourceVersionId = sourceQuery?.versionId;
3543

36-
return { sourceBucket, sourceObject, sourceVersionId };
44+
return { sourceBucket, sourceObject, sourceVersionId, copySourceVersionId };
3745
}
3846

3947
module.exports = parseCopySource;

‎lib/api/objectCopy.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ function objectCopy(authInfo, request, sourceBucket, sourceObject, sourceVersion
244244
checkQuota: true,
245245
request,
246246
// The request targets the destination, the bucket policy must be
247-
// evaluated against the source
248-
policyTarget: { bucketName: sourceBucket, objectKey: sourceObject },
247+
// evaluated against the source, with its version ID as sent
248+
policyTarget: { bucketName: sourceBucket, objectKey: sourceObject, versionId: request.copySourceVersionId },
249249
};
250250
const valPutParams = {
251251
authInfo,

‎lib/api/objectPutCopyPart.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ function objectPutCopyPart(authInfo, request, sourceBucket, sourceObject, reqVer
5757
checkQuota: true,
5858
request,
5959
// The request targets the destination, the bucket policy must be
60-
// evaluated against the source
61-
policyTarget: { bucketName: sourceBucket, objectKey: sourceObject },
60+
// evaluated against the source, with its version ID as sent
61+
policyTarget: { bucketName: sourceBucket, objectKey: sourceObject, versionId: request.copySourceVersionId },
6262
};
6363

6464
const partNumber = Number.parseInt(request.query.partNumber, 10);

‎lib/metadata/metadataUtils.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ function checkRateLimitIfNeeded(request, authInfo, bucketMD, log, callback) {
357357
* when it differs from the request target, e.g. the source of a copy
358358
* @param {string} params.policyTarget.bucketName - name of bucket
359359
* @param {string} params.policyTarget.objectKey - name of object
360+
* @param {string} [params.policyTarget.versionId] - version ID as sent by the client
360361
* @param {boolean} actionImplicitDenies - identity authorization results
361362
* @param {RequestLogger} log - request logger
362363
* @param {function} callback - callback

0 commit comments

Comments
 (0)