From 011852d091638865a80331dd0cc0aec214eb7d6b Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 13 Jul 2026 00:27:22 +0200 Subject: [PATCH] fix(web): close prototype-chain bypass in DELETE /api/config allowlist FIELDS[field] walked the prototype chain, so field=__proto__ (or constructor, hasOwnProperty, etc.) resolved to a truthy inherited value whose .key was undefined. Prisma drops undefined filter values, so deleteMany({ where: { key: undefined } }) collapsed to deleteMany({}), wiping the entire Config table. Guard with an own-property check before indexing FIELDS so only real allowlist entries pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/app/api/config/route.test.ts | 15 +++++++++++++++ web/src/app/api/config/route.ts | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/web/src/app/api/config/route.test.ts b/web/src/app/api/config/route.test.ts index 7f0bf97..380eb21 100644 --- a/web/src/app/api/config/route.test.ts +++ b/web/src/app/api/config/route.test.ts @@ -76,8 +76,23 @@ describe("config DELETE", () => { }); it("rejects an unknown field with 400 and deletes nothing", async () => { + await PUT(putReq({ qobuzPassword: "hunter2" })); const res = await DELETE(delReq("monitor.enabled")); expect(res.status).toBe(400); + expect(await prisma.config.findUnique({ where: { key: "qobuz.password" } })).not.toBeNull(); + }); + + it("rejects prototype-chain field names (__proto__, constructor, etc.) with 400 and deletes nothing", async () => { + await PUT(putReq({ qobuzPassword: "hunter2" })); + const before = await prisma.config.count(); + expect(before).toBeGreaterThan(0); + + const res = await DELETE(delReq("__proto__")); + expect(res.status).toBe(400); + + // Must not have collapsed to deleteMany({ where: {} }) and wiped the table. + expect(await prisma.config.count()).toBe(before); + expect(await prisma.config.findUnique({ where: { key: "qobuz.password" } })).not.toBeNull(); }); it("is idempotent — deleting an absent field returns 200", async () => { diff --git a/web/src/app/api/config/route.ts b/web/src/app/api/config/route.ts index 440d215..65a531b 100644 --- a/web/src/app/api/config/route.ts +++ b/web/src/app/api/config/route.ts @@ -48,10 +48,10 @@ export async function GET() { export async function DELETE(request: Request) { const field = new URL(request.url).searchParams.get("field") ?? ""; - const entry = FIELDS[field]; - if (!entry) { + if (!Object.prototype.hasOwnProperty.call(FIELDS, field)) { return Response.json({ error: "unknown field" }, { status: 400 }); } + const entry = FIELDS[field]; await prisma.config.deleteMany({ where: { key: entry.key } }); // idempotent return Response.json({ ok: true }); }