Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/handle-malformed-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"r2-explorer": patch
---

Return 400 instead of 500 when upload endpoints receive malformed base64-encoded metadata in `customMetadata` or `httpMetadata` query parameters
25 changes: 19 additions & 6 deletions packages/worker/src/modules/buckets/multipart/createUpload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OpenAPIRoute } from "chanfana";
import { HTTPException } from "hono/http-exception";
import { z } from "zod";
import type { AppContext } from "../../../types";

Expand Down Expand Up @@ -47,16 +48,28 @@ export class CreateUpload extends OpenAPIRoute {

let customMetadata = undefined;
if (data.query.customMetadata) {
customMetadata = JSON.parse(
decodeURIComponent(escape(atob(data.query.customMetadata))),
);
try {
customMetadata = JSON.parse(
decodeURIComponent(escape(atob(data.query.customMetadata))),
);
} catch {
throw new HTTPException(400, {
message: "Invalid customMetadata: expected base64-encoded JSON",
});
}
}

let httpMetadata = undefined;
if (data.query.httpMetadata) {
httpMetadata = JSON.parse(
decodeURIComponent(escape(atob(data.query.httpMetadata))),
);
try {
httpMetadata = JSON.parse(
decodeURIComponent(escape(atob(data.query.httpMetadata))),
);
} catch {
throw new HTTPException(400, {
message: "Invalid httpMetadata: expected base64-encoded JSON",
});
}
}

return await bucket.createMultipartUpload(key, {
Expand Down
24 changes: 18 additions & 6 deletions packages/worker/src/modules/buckets/putObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,28 @@ export class PutObject extends OpenAPIRoute {

let customMetadata = undefined;
if (data.query.customMetadata) {
customMetadata = JSON.parse(
decodeURIComponent(escape(atob(data.query.customMetadata))),
);
try {
customMetadata = JSON.parse(
decodeURIComponent(escape(atob(data.query.customMetadata))),
);
} catch {
throw new HTTPException(400, {
message: "Invalid customMetadata: expected base64-encoded JSON",
});
}
}

let httpMetadata = undefined;
if (data.query.httpMetadata) {
httpMetadata = JSON.parse(
decodeURIComponent(escape(atob(data.query.httpMetadata))),
);
try {
httpMetadata = JSON.parse(
decodeURIComponent(escape(atob(data.query.httpMetadata))),
);
} catch {
throw new HTTPException(400, {
message: "Invalid httpMetadata: expected base64-encoded JSON",
});
}
}

return await bucket.put(key, c.req.raw.body, {
Expand Down
50 changes: 50 additions & 0 deletions packages/worker/tests/integration/buckets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,56 @@ describe("Bucket Endpoints", () => {
expect(await r2Object?.text()).toBe(newObjectContent);
});

it("should return 400 for malformed customMetadata", async () => {
if (!MY_TEST_BUCKET_1)
throw new Error("MY_TEST_BUCKET_1 binding not available");

const objectKey = "bad-metadata.txt";
const base64ObjectKey = btoa(objectKey);
const blobBody = new Blob(["content"], {
type: "application/octet-stream",
});

const badCustomMetadata = btoa("not-valid-json");

const request = createTestRequest(
`/api/buckets/MY_TEST_BUCKET_1/upload?key=${encodeURIComponent(base64ObjectKey)}&customMetadata=${encodeURIComponent(badCustomMetadata)}`,
"POST",
blobBody,
{ "Content-Type": "application/octet-stream" },
);

const response = await app.fetch(request, env, createExecutionContext());
expect(response.status).toBe(400);
const body = await response.text();
expect(body).toContain("Invalid customMetadata");
});

it("should return 400 for malformed httpMetadata", async () => {
if (!MY_TEST_BUCKET_1)
throw new Error("MY_TEST_BUCKET_1 binding not available");

const objectKey = "bad-metadata.txt";
const base64ObjectKey = btoa(objectKey);
const blobBody = new Blob(["content"], {
type: "application/octet-stream",
});

const badHttpMetadata = btoa("{broken json");

const request = createTestRequest(
`/api/buckets/MY_TEST_BUCKET_1/upload?key=${encodeURIComponent(base64ObjectKey)}&httpMetadata=${encodeURIComponent(badHttpMetadata)}`,
"POST",
blobBody,
{ "Content-Type": "application/octet-stream" },
);

const response = await app.fetch(request, env, createExecutionContext());
expect(response.status).toBe(400);
const body = await response.text();
expect(body).toContain("Invalid httpMetadata");
});

it("POST /api/buckets/NON_EXISTENT_BUCKET/upload - should return 500 if bucket binding does not exist", async () => {
const base64ObjectKey = btoa("test.txt");
const blobBody = new Blob(["content"], {
Expand Down
42 changes: 42 additions & 0 deletions packages/worker/tests/integration/multipart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,48 @@ describe("Multipart Upload Endpoints", () => {
const response = await app.fetch(request, env, createExecutionContext());
expect(response.status).toBe(400);
});

it("should return 400 for malformed customMetadata", async () => {
if (!MY_TEST_BUCKET_1)
throw new Error("MY_TEST_BUCKET_1 not available");

const objectKey = "bad-metadata.dat";
const base64ObjectKey = btoa(objectKey);
const badCustomMetadata = btoa("not-valid-json");

const request = createTestRequest(
`/api/buckets/${BUCKET_NAME}/multipart/create?key=${encodeURIComponent(base64ObjectKey)}&customMetadata=${encodeURIComponent(badCustomMetadata)}`,
"POST",
undefined,
{ "Content-Type": "application/json" },
);

const response = await app.fetch(request, env, createExecutionContext());
expect(response.status).toBe(400);
const body = await response.text();
expect(body).toContain("Invalid customMetadata");
});

it("should return 400 for malformed httpMetadata", async () => {
if (!MY_TEST_BUCKET_1)
throw new Error("MY_TEST_BUCKET_1 not available");

const objectKey = "bad-metadata.dat";
const base64ObjectKey = btoa(objectKey);
const badHttpMetadata = btoa("{broken json");

const request = createTestRequest(
`/api/buckets/${BUCKET_NAME}/multipart/create?key=${encodeURIComponent(base64ObjectKey)}&httpMetadata=${encodeURIComponent(badHttpMetadata)}`,
"POST",
undefined,
{ "Content-Type": "application/json" },
);

const response = await app.fetch(request, env, createExecutionContext());
expect(response.status).toBe(400);
const body = await response.text();
expect(body).toContain("Invalid httpMetadata");
});
});

describe("PartUpload (POST /api/buckets/:bucket/multipart/upload)", () => {
Expand Down
Loading