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 }); }