diff --git a/ChangeLog.md b/ChangeLog.md index 900ec37a2..bbd3a453c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -16,6 +16,7 @@ General: Blob: +- Fixed container-scoped Blob Batch requests accepting subrequests that target a sibling container whose name starts with the scope container's name, e.g. a batch scoped to `logs` deleting a blob in `logs-archive`. Subrequests must now match the container of the request URI exactly and are rejected with `InvalidInput`, matching Azure Storage. (issue #2884) - Fixed Stage Block responses for API version `2019-02-02` and later to return `Content-MD5` when the request supplies `Content-MD5`, matching Azure Storage while preserving CRC64 responses for requests without MD5. (issue #2394) - Bumped the default Blob service API version to `2026-10-06`, and added `2026-10-06` to the supported Blob service API versions. - Fixed Blob GET and HEAD requests for nonexistent blobs with `If-Match` so they now return `BlobNotFound` instead of `ConditionNotMet`, matching Azure Storage. (issue #2751) diff --git a/src/blob/handlers/BlobBatchHandler.ts b/src/blob/handlers/BlobBatchHandler.ts index a0cb545b8..49bd8b1f8 100644 --- a/src/blob/handlers/BlobBatchHandler.ts +++ b/src/blob/handlers/BlobBatchHandler.ts @@ -402,7 +402,16 @@ export class BlobBatchHandler { const requestPath = operationInfos[1].startsWith("/") ? operationInfos[1] : "/" + operationInfos[1]; - if (!requestPath.startsWith(subRequestPathPrefix)) { + // The scope is a path prefix, so a plain startsWith() also accepts sibling + // containers whose name merely starts with the scope container's name + // ("/container" would accept "/container-archive"). The matched prefix must + // end at a path separator to be the same container. An empty prefix means + // the batch is scoped to the whole account and accepts every path. + const isSameContainer = requestPath.startsWith(subRequestPathPrefix) + && (requestPath.length === subRequestPathPrefix.length + || requestPath[subRequestPathPrefix.length] === "/"); + + if (!isSameContainer) { throw new Error("Request from a different container"); } diff --git a/tests/blob/apis/blobbatch.test.ts b/tests/blob/apis/blobbatch.test.ts index 0eb325857..ce3b2b2c2 100644 --- a/tests/blob/apis/blobbatch.test.ts +++ b/tests/blob/apis/blobbatch.test.ts @@ -401,6 +401,31 @@ describe("Blob batch API", () => { assert.equal(resp.subResponsesFailedCount, 1); }); + it("SubmitBatch within container scope - batch deleting blob in a container whose name starts with the scope container name @loki @sql", async () => { + const blobBatchClient = containerClient.getBlobBatchClient(); + + // "containerX-archive" starts with "containerX", but it is a different container. + const siblingContainerClient = serviceClient.getContainerClient(`${containerName}-archive`); + await siblingContainerClient.create(); + + const siblingBlobClient = siblingContainerClient.getBlockBlobClient(getUniqueName("blob")); + await siblingBlobClient.upload(content, content.length); + + const sharedKeyCredential = (serviceClient as any).credential as StorageSharedKeyCredential; + + try { + const resp = await blobBatchClient.deleteBlobs([siblingBlobClient.url], sharedKeyCredential, {}); + assert.equal(resp.subResponses.length, 1); + assert.equal(resp.subResponsesSucceededCount, 0); + assert.equal(resp.subResponsesFailedCount, 1); + + // The subrequest must not have been executed. + await siblingBlobClient.getProperties(); + } finally { + await siblingContainerClient.delete(); + } + }); + it("SubmitBatch with SAS token - batch deleting @loki @sql", async () => { const tmr = new Date(); tmr.setDate(tmr.getDate() + 1);