Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 10 additions & 1 deletion src/blob/handlers/BlobBatchHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
25 changes: 25 additions & 0 deletions tests/blob/apis/blobbatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down