From 132583f0d798c4e598c7c0b6d7899b28377b4e3c Mon Sep 17 00:00:00 2001 From: Gabriel Massadas Date: Sat, 7 Mar 2026 17:47:26 +0000 Subject: [PATCH 1/2] Fix Content-Disposition header injection in GetObject The filename in the Content-Disposition header was interpolated directly without sanitization. Filenames containing double quotes could break header parsing or enable header injection. This fix: - Strips non-ASCII characters and replaces double quotes in the ASCII `filename` parameter for compatibility - Adds RFC 5987 `filename*` parameter with proper UTF-8 percent encoding, matching the approach used in getShareLink.ts Co-Authored-By: Claude Opus 4.6 --- packages/worker/src/modules/buckets/getObject.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/worker/src/modules/buckets/getObject.ts b/packages/worker/src/modules/buckets/getObject.ts index abfba8a4..2b5eb660 100644 --- a/packages/worker/src/modules/buckets/getObject.ts +++ b/packages/worker/src/modules/buckets/getObject.ts @@ -57,9 +57,14 @@ export class GetObject extends OpenAPIRoute { object.writeHttpMetadata(headers); headers.set("etag", object.httpEtag); headers.set("content-length", object.size.toString()); + + const fileName = filePath.split("/").pop() || "download"; + const asciiFileName = fileName + .replace(/[^\x20-\x7E]/g, "_") + .replace(/"/g, "'"); headers.set( "Content-Disposition", - `attachment; filename="${filePath.split("/").pop()}"`, + `attachment; filename="${asciiFileName}"; filename*=UTF-8''${encodeURIComponent(fileName)}`, ); return new Response(object.body, { From e69167239867d6bfd50dd6330245b12e526b1271 Mon Sep 17 00:00:00 2001 From: Gabriel Massadas Date: Mon, 9 Mar 2026 10:41:13 +0000 Subject: [PATCH 2/2] Fix CI: update test for new Content-Disposition format and add changeset - Update GetObject test to expect the sanitized filename with RFC 5987 filename* parameter added by the header injection fix - Add required changeset for the patch release Co-Authored-By: Claude Opus 4.6 --- .changeset/fix-content-disposition-injection.md | 5 +++++ packages/worker/tests/integration/object.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-content-disposition-injection.md diff --git a/.changeset/fix-content-disposition-injection.md b/.changeset/fix-content-disposition-injection.md new file mode 100644 index 00000000..8ad6644f --- /dev/null +++ b/.changeset/fix-content-disposition-injection.md @@ -0,0 +1,5 @@ +--- +"r2-explorer": patch +--- + +Fix Content-Disposition header injection in GetObject by sanitizing filenames (replacing non-ASCII with `_` and double quotes with `'`) and adding RFC 5987 `filename*=UTF-8''...` parameter for proper Unicode support. diff --git a/packages/worker/tests/integration/object.test.ts b/packages/worker/tests/integration/object.test.ts index 3771e65d..eb7f82a5 100644 --- a/packages/worker/tests/integration/object.test.ts +++ b/packages/worker/tests/integration/object.test.ts @@ -147,8 +147,8 @@ describe("Object Specific Endpoints", () => { expect(response.headers.get("content-type")).toBe(TEST_OBJECT_CONTENT_TYPE); expect(response.headers.get("content-length")).toBe(TEST_OBJECT_CONTENT.length.toString()); expect(response.headers.has("etag")).toBe(true); - // Default R2 GetObject includes Content-Disposition: attachment; filename="key" - expect(response.headers.get("content-disposition")).toBe(`attachment; filename="${TEST_OBJECT_KEY}"`); + // Default R2 GetObject includes Content-Disposition with sanitized filename and RFC 5987 filename* + expect(response.headers.get("content-disposition")).toBe(`attachment; filename="${TEST_OBJECT_KEY}"; filename*=UTF-8''${encodeURIComponent(TEST_OBJECT_KEY)}`); const body = await response.text(); expect(body).toBe(TEST_OBJECT_CONTENT);