diff --git a/web/src/app/api/config/route.test.ts b/web/src/app/api/config/route.test.ts index 380eb21..e491f3a 100644 --- a/web/src/app/api/config/route.test.ts +++ b/web/src/app/api/config/route.test.ts @@ -87,9 +87,10 @@ describe("config DELETE", () => { const before = await prisma.config.count(); expect(before).toBeGreaterThan(0); - const res = await DELETE(delReq("__proto__")); - expect(res.status).toBe(400); - + for (const bad of ["__proto__", "constructor"]) { + const res = await DELETE(delReq(bad)); + 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(); diff --git a/web/src/app/api/discover/config/route.test.ts b/web/src/app/api/discover/config/route.test.ts index 86b7adf..efe89af 100644 --- a/web/src/app/api/discover/config/route.test.ts +++ b/web/src/app/api/discover/config/route.test.ts @@ -26,4 +26,11 @@ describe("discover config API", () => { expect(body["discover.chunkSize"]).toBe("25"); expect(await prisma.config.findUnique({ where: { key: "nope" } })).toBeNull(); }); + + it("ignores prototype-chain keys (__proto__, constructor) and writes no Config row", async () => { + const before = await prisma.config.count(); + const res = await patch({ __proto__: "x", constructor: "y" }); + expect((await res.json()).updated).toBe(0); + expect(await prisma.config.count()).toBe(before); + }); }); diff --git a/web/src/app/api/discover/config/route.ts b/web/src/app/api/discover/config/route.ts index 69094a6..3726378 100644 --- a/web/src/app/api/discover/config/route.ts +++ b/web/src/app/api/discover/config/route.ts @@ -26,7 +26,7 @@ export async function PATCH(request: Request) { return Response.json({ error: "invalid JSON" }, { status: 400 }); } const entries = Object.entries((body ?? {}) as Record).filter( - ([k]) => k in DEFAULTS, + ([k]) => Object.prototype.hasOwnProperty.call(DEFAULTS, k), ); for (const [key, value] of entries) { await prisma.config.upsert({ diff --git a/web/src/app/api/monitor/config/route.test.ts b/web/src/app/api/monitor/config/route.test.ts index 90be874..cec2a27 100644 --- a/web/src/app/api/monitor/config/route.test.ts +++ b/web/src/app/api/monitor/config/route.test.ts @@ -26,4 +26,11 @@ describe("monitor config API", () => { expect(body["monitor.pollIntervalHours"]).toBe("12"); expect(await prisma.config.findUnique({ where: { key: "nope" } })).toBeNull(); }); + + it("ignores prototype-chain keys (__proto__, constructor) and writes no Config row", async () => { + const before = await prisma.config.count(); + const res = await patch({ __proto__: "x", constructor: "y" }); + expect((await res.json()).updated).toBe(0); + expect(await prisma.config.count()).toBe(before); + }); }); diff --git a/web/src/app/api/monitor/config/route.ts b/web/src/app/api/monitor/config/route.ts index 4eaca77..e92bc0e 100644 --- a/web/src/app/api/monitor/config/route.ts +++ b/web/src/app/api/monitor/config/route.ts @@ -24,7 +24,7 @@ export async function PATCH(request: Request) { return Response.json({ error: "invalid JSON" }, { status: 400 }); } const entries = Object.entries((body ?? {}) as Record).filter( - ([k]) => k in DEFAULTS, + ([k]) => Object.prototype.hasOwnProperty.call(DEFAULTS, k), ); for (const [key, value] of entries) { await prisma.config.upsert({