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:
@@ -76,8 +76,23 @@ describe("config DELETE", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("rejects an unknown field with 400 and deletes nothing", async () => {
|
it("rejects an unknown field with 400 and deletes nothing", async () => {
|
||||||
|
await PUT(putReq({ qobuzPassword: "hunter2" }));
|
||||||
const res = await DELETE(delReq("monitor.enabled"));
|
const res = await DELETE(delReq("monitor.enabled"));
|
||||||
expect(res.status).toBe(400);
|
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 () => {
|
it("is idempotent — deleting an absent field returns 200", async () => {
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ export async function GET() {
|
|||||||
|
|
||||||
export async function DELETE(request: Request) {
|
export async function DELETE(request: Request) {
|
||||||
const field = new URL(request.url).searchParams.get("field") ?? "";
|
const field = new URL(request.url).searchParams.get("field") ?? "";
|
||||||
const entry = FIELDS[field];
|
if (!Object.prototype.hasOwnProperty.call(FIELDS, field)) {
|
||||||
if (!entry) {
|
|
||||||
return Response.json({ error: "unknown field" }, { status: 400 });
|
return Response.json({ error: "unknown field" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
const entry = FIELDS[field];
|
||||||
await prisma.config.deleteMany({ where: { key: entry.key } }); // idempotent
|
await prisma.config.deleteMany({ where: { key: entry.key } }); // idempotent
|
||||||
return Response.json({ ok: true });
|
return Response.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user