Skip to content

Commit 3f64420

Browse files
authored
Fix issue of copy succeeded without 'r' permission in SAS token credential (#2330)
1 parent 9208664 commit 3f64420

File tree

4 files changed

+110
-67
lines changed

4 files changed

+110
-67
lines changed

ChangeLog.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Blob:
88

99
- Fixed issue of setting blob tag should not update Blob Etag and LastModified. (issue #2327)
1010
- Fix HTTP header parsing of `SubmitBatch()`. If a HTTP header has HTTP header delimiter (`:`) in its value, `SubmitBatch()` returns "400 One of the request inputs is not valid". For example, if `user-agent` header is `azsdk-cpp-storage-blobs/12.10.0-beta.1 (Darwin 23.1.0 arm64 Darwin Kernel Version 23.1.0: Mon Oct 9 21:28:12 PDT 2023; root:xnu-10002.41.9~6/RELEASE_ARM64_T8103)`, all `SubmitBatch()` requests are failed.
11+
- Fixed issue of blob copying succeed without 'r' permission in source blob's SAS token credential.
1112

1213
## 2023.12 Version 3.29.0
1314

src/blob/handlers/BlobHandler.ts

+63-64
Original file line numberDiff line numberDiff line change
@@ -122,39 +122,39 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
122122

123123
const response: Models.BlobGetPropertiesResponse = againstMetadata
124124
? {
125-
statusCode: 200,
126-
metadata: res.metadata,
127-
eTag: res.properties.etag,
128-
requestId: context.contextId,
129-
version: BLOB_API_VERSION,
130-
date: context.startTime,
131-
clientRequestId: options.requestId,
132-
contentLength: res.properties.contentLength,
133-
lastModified: res.properties.lastModified
134-
}
125+
statusCode: 200,
126+
metadata: res.metadata,
127+
eTag: res.properties.etag,
128+
requestId: context.contextId,
129+
version: BLOB_API_VERSION,
130+
date: context.startTime,
131+
clientRequestId: options.requestId,
132+
contentLength: res.properties.contentLength,
133+
lastModified: res.properties.lastModified
134+
}
135135
: {
136-
statusCode: 200,
137-
metadata: res.metadata,
138-
isIncrementalCopy: res.properties.incrementalCopy,
139-
eTag: res.properties.etag,
140-
requestId: context.contextId,
141-
version: BLOB_API_VERSION,
142-
date: context.startTime,
143-
acceptRanges: "bytes",
144-
blobCommittedBlockCount:
145-
res.properties.blobType === Models.BlobType.AppendBlob
146-
? res.blobCommittedBlockCount
147-
: undefined,
148-
isServerEncrypted: true,
149-
clientRequestId: options.requestId,
150-
...res.properties,
151-
cacheControl: context.request!.getQuery("rscc") ?? res.properties.cacheControl,
152-
contentDisposition: context.request!.getQuery("rscd") ?? res.properties.contentDisposition,
153-
contentEncoding: context.request!.getQuery("rsce") ?? res.properties.contentEncoding,
154-
contentLanguage: context.request!.getQuery("rscl") ?? res.properties.contentLanguage,
155-
contentType: context.request!.getQuery("rsct") ?? res.properties.contentType,
156-
tagCount: res.properties.tagCount,
157-
};
136+
statusCode: 200,
137+
metadata: res.metadata,
138+
isIncrementalCopy: res.properties.incrementalCopy,
139+
eTag: res.properties.etag,
140+
requestId: context.contextId,
141+
version: BLOB_API_VERSION,
142+
date: context.startTime,
143+
acceptRanges: "bytes",
144+
blobCommittedBlockCount:
145+
res.properties.blobType === Models.BlobType.AppendBlob
146+
? res.blobCommittedBlockCount
147+
: undefined,
148+
isServerEncrypted: true,
149+
clientRequestId: options.requestId,
150+
...res.properties,
151+
cacheControl: context.request!.getQuery("rscc") ?? res.properties.cacheControl,
152+
contentDisposition: context.request!.getQuery("rscd") ?? res.properties.contentDisposition,
153+
contentEncoding: context.request!.getQuery("rsce") ?? res.properties.contentEncoding,
154+
contentLanguage: context.request!.getQuery("rscl") ?? res.properties.contentLanguage,
155+
contentType: context.request!.getQuery("rsct") ?? res.properties.contentType,
156+
tagCount: res.properties.tagCount,
157+
};
158158

159159
return response;
160160
}
@@ -329,12 +329,10 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
329329
const metadata = convertRawHeadersToMetadata(
330330
blobCtx.request!.getRawHeaders()
331331
);
332-
333-
if (metadata != undefined)
334-
{
332+
333+
if (metadata != undefined) {
335334
Object.entries(metadata).forEach(([key, value]) => {
336-
if (key.includes("-"))
337-
{
335+
if (key.includes("-")) {
338336
throw StorageErrorFactory.getInvalidMetadata(context.contextId!);
339337
}
340338
});
@@ -664,7 +662,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
664662
throw StorageErrorFactory.getBlobNotFound(context.contextId!);
665663
}
666664

667-
if (sourceAccount !== blobCtx.account) {
665+
const sig = url.searchParams.get("sig");
666+
if ((sourceAccount !== blobCtx.account) || (sig !== null)) {
668667
await this.validateCopySource(copySource, sourceAccount, context);
669668
}
670669

@@ -1103,7 +1102,7 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
11031102
tagCount: getBlobTagsCount(blob.blobTags),
11041103
isServerEncrypted: true,
11051104
clientRequestId: options.requestId,
1106-
creationTime:blob.properties.creationTime,
1105+
creationTime: blob.properties.creationTime,
11071106
blobCommittedBlockCount:
11081107
blob.properties.blobType === Models.BlobType.AppendBlob
11091108
? (blob.committedBlocksInOrder || []).length
@@ -1176,9 +1175,9 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
11761175
contentLength <= 0
11771176
? []
11781177
: this.rangesManager.fillZeroRanges(blob.pageRangesInOrder, {
1179-
start: rangeStart,
1180-
end: rangeEnd
1181-
});
1178+
start: rangeStart,
1179+
end: rangeEnd
1180+
});
11821181

11831182
const bodyGetter = async () => {
11841183
return this.extentStore.readExtents(
@@ -1233,7 +1232,7 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
12331232
blobContentMD5: blob.properties.contentMD5,
12341233
tagCount: getBlobTagsCount(blob.blobTags),
12351234
isServerEncrypted: true,
1236-
creationTime:blob.properties.creationTime,
1235+
creationTime: blob.properties.creationTime,
12371236
clientRequestId: options.requestId
12381237
};
12391238

@@ -1243,7 +1242,7 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
12431242
public async query(
12441243
options: Models.BlobQueryOptionalParams,
12451244
context: Context
1246-
): Promise<Models.BlobQueryResponse>{
1245+
): Promise<Models.BlobQueryResponse> {
12471246
throw new NotImplementedError(context.contextId);
12481247
}
12491248

@@ -1266,13 +1265,13 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
12661265
);
12671266

12681267
const response: Models.BlobGetTagsResponse = {
1269-
statusCode: 200,
1270-
blobTagSet: tags === undefined ? []: tags.blobTagSet,
1271-
requestId: context.contextId,
1272-
version: BLOB_API_VERSION,
1273-
date: context.startTime,
1274-
clientRequestId: options.requestId,
1275-
};
1268+
statusCode: 200,
1269+
blobTagSet: tags === undefined ? [] : tags.blobTagSet,
1270+
requestId: context.contextId,
1271+
version: BLOB_API_VERSION,
1272+
date: context.startTime,
1273+
clientRequestId: options.requestId,
1274+
};
12761275

12771276
return response;
12781277
}
@@ -1315,18 +1314,18 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
13151314
return response;
13161315
}
13171316

1318-
private NewUriFromCopySource(copySource: string, context: Context): URL{
1319-
try{
1320-
return new URL(copySource)
1321-
}
1322-
catch
1323-
{
1324-
throw StorageErrorFactory.getInvalidHeaderValue(
1325-
context.contextId,
1326-
{
1327-
HeaderName: "x-ms-copy-source",
1328-
HeaderValue: copySource
1329-
})
1330-
}
1317+
private NewUriFromCopySource(copySource: string, context: Context): URL {
1318+
try {
1319+
return new URL(copySource)
1320+
}
1321+
catch
1322+
{
1323+
throw StorageErrorFactory.getInvalidHeaderValue(
1324+
context.contextId,
1325+
{
1326+
HeaderName: "x-ms-copy-source",
1327+
HeaderValue: copySource
1328+
})
1329+
}
13311330
}
13321331
}

tests/blob/apis/blockblob.test.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {
22
StorageSharedKeyCredential,
33
BlobServiceClient,
4-
newPipeline
4+
newPipeline,
5+
BlobSASPermissions
56
} from "@azure/storage-blob";
67
import assert = require("assert");
78
import crypto = require("crypto");
@@ -548,4 +549,45 @@ describe("BlockBlobAPIs", () => {
548549
body
549550
);
550551
});
552+
553+
it("Start copy without required permission should fail @loki @sql", async () => {
554+
const body: string = getUniqueName("randomstring");
555+
const expiryTime = new Date();
556+
expiryTime.setDate(expiryTime.getDate() + 1);
557+
await blockBlobClient.upload(body, Buffer.byteLength(body));
558+
559+
const sourceURLWithoutPermission = await blockBlobClient.generateSasUrl({
560+
permissions: BlobSASPermissions.parse("w"),
561+
expiresOn: expiryTime
562+
});
563+
564+
const destBlobName: string = getUniqueName("destBlobName");
565+
const destBlobClient = containerClient.getBlockBlobClient(destBlobName);
566+
567+
try {
568+
await destBlobClient.beginCopyFromURL(sourceURLWithoutPermission);
569+
assert.fail("Copy without required permision should fail");
570+
}
571+
catch (ex) {
572+
assert.deepStrictEqual(ex.statusCode, 403);
573+
assert.ok(ex.message.startsWith("This request is not authorized to perform this operation using this permission."));
574+
assert.deepStrictEqual(ex.code, "CannotVerifyCopySource");
575+
}
576+
577+
// Copy within the same account without SAS token should succeed.
578+
const result = await (await destBlobClient.beginCopyFromURL(blockBlobClient.url)).pollUntilDone();
579+
assert.ok(result.copyId);
580+
assert.strictEqual(result.errorCode, undefined);
581+
582+
// Copy with 'r' permission should succeed.
583+
const sourceURL = await blockBlobClient.generateSasUrl({
584+
permissions: BlobSASPermissions.parse("r"),
585+
expiresOn: expiryTime
586+
});
587+
588+
const resultWithPermission = await (await destBlobClient.beginCopyFromURL(sourceURL)).pollUntilDone();
589+
assert.ok(resultWithPermission.copyId);
590+
assert.strictEqual(resultWithPermission.errorCode, undefined);
591+
});
592+
551593
});

tests/blob/sas.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -655,15 +655,16 @@ describe("Shared Access Signature (SAS) authentication", () => {
655655
);
656656

657657
const containerName = getUniqueName("con");
658-
const containerClient = serviceClientWithSAS.getContainerClient(
658+
const containerClient = serviceClient.getContainerClient(containerName);
659+
const containerClientWithSAS = serviceClientWithSAS.getContainerClient(
659660
containerName
660661
);
661662
await containerClient.create();
662663

663664
const blobName1 = getUniqueName("blob");
664665
const blobName2 = getUniqueName("blob");
665666
const blob1 = containerClient.getBlockBlobClient(blobName1);
666-
const blob2 = containerClient.getBlockBlobClient(blobName2);
667+
const blob2 = containerClientWithSAS.getBlockBlobClient(blobName2);
667668

668669
await blob1.upload("hello", 5);
669670

0 commit comments

Comments
 (0)