From 2a576427309b3b9509a78447517f8be1bb04b962 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:05:23 +0000 Subject: [PATCH 1/2] Initial plan From a2e1caad8e490d6952c97f926e5d1931023453a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:15:46 +0000 Subject: [PATCH 2/2] Handle malformed upload form-data as 400 --- src/app/api/upload/route.ts | 12 ++++++-- tests/api-validation-contract.test.ts | 40 +++++++++++++++++++++++++++ tests/private-access-routes.test.ts | 4 +-- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 6b1e425fda..c7dcf0db54 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -3,7 +3,7 @@ import { createHash } from "node:crypto"; import { NextResponse } from "next/server"; import { z } from "zod"; import { env } from "@/lib/env"; -import { assertAllowedFile, assertFileContentSignature, jsonError } from "@/lib/http"; +import { assertAllowedFile, assertFileContentSignature, jsonError, PublicApiError } from "@/lib/http"; import { logger } from "@/lib/logger"; import { writeAuditLog } from "@/lib/audit"; import { planDocumentName } from "@/lib/document-naming"; @@ -28,7 +28,13 @@ export async function POST(request: Request) { try { supabase = createAdminClient(); const user = await requireAuthenticatedUser(request, supabase); - const formData = await request.formData(); + const formData = await request.formData().catch((cause) => { + throw new PublicApiError("Invalid upload form data.", 400, { + code: "invalid_form_data", + causeName: cause instanceof Error ? cause.name : null, + causeMessage: cause instanceof Error ? cause.message : null, + }); + }); const file = formData.get("file"); if (!(file instanceof File)) { return NextResponse.json({ error: "Missing file field." }, { status: 400 }); @@ -174,6 +180,6 @@ export async function POST(request: Request) { return unauthorizedResponse(); } - return jsonError(error, 400); + return jsonError(error); } } diff --git a/tests/api-validation-contract.test.ts b/tests/api-validation-contract.test.ts index 00a107f109..7d534acefc 100644 --- a/tests/api-validation-contract.test.ts +++ b/tests/api-validation-contract.test.ts @@ -386,6 +386,46 @@ describe("API validation contracts", () => { expect(client.from).not.toHaveBeenCalled(); }); + it("rejects non-form upload content before storage upload or database writes", async () => { + const client = createSupabaseMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/upload/route"); + + const response = await POST( + authenticatedRequest("/api/upload", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ file: "nope" }), + }), + ); + const body = await payload(response); + + expect(response.status).toBe(400); + expect(body).toEqual({ error: "Invalid upload form data." }); + expect(client.storageMocks.upload).not.toHaveBeenCalled(); + expect(client.from).not.toHaveBeenCalled(); + }); + + it("rejects malformed multipart upload bodies before storage upload or database writes", async () => { + const client = createSupabaseMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/upload/route"); + + const response = await POST( + authenticatedRequest("/api/upload", { + method: "POST", + headers: { "content-type": "multipart/form-data; boundary=broken" }, + body: "--broken\r\nContent-Disposition: form-data; name=\"file\"; filename=\"guideline.pdf\"\r\n\r\n%PDF-1.7", + }), + ); + const body = await payload(response); + + expect(response.status).toBe(400); + expect(body).toEqual({ error: "Invalid upload form data." }); + expect(client.storageMocks.upload).not.toHaveBeenCalled(); + expect(client.from).not.toHaveBeenCalled(); + }); + it("accepts valid document rename JSON through the shared body parser", async () => { const client = createSupabaseMock((call) => { if (call.table === "documents" && call.operation === "select" && call.maybeSingle) { diff --git a/tests/private-access-routes.test.ts b/tests/private-access-routes.test.ts index ce2291fc7d..7f0526b37f 100644 --- a/tests/private-access-routes.test.ts +++ b/tests/private-access-routes.test.ts @@ -1489,7 +1489,7 @@ describe("private document API access", () => { ); const uploadPath = client.storageMocks.upload.mock.calls[0]?.[0] as string; - expect(response.status).toBe(400); + expect(response.status).toBe(500); expect(client.storageMocks.remove).toHaveBeenCalledWith([uploadPath]); }); @@ -1516,7 +1516,7 @@ describe("private document API access", () => { ); const uploadPath = client.storageMocks.upload.mock.calls[0]?.[0] as string; - expect(response.status).toBe(400); + expect(response.status).toBe(500); expect(client.storageMocks.remove).toHaveBeenCalledWith([uploadPath]); });