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) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-13 00:27:22 +02:00
parent 789eb2c76a
commit 011852d091
2 changed files with 17 additions and 2 deletions
+15
View File
@@ -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 () => {
+2 -2
View File
@@ -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 });
}