From e8f09f240e2a4dfc5132da2bc920113d4205a6f4 Mon Sep 17 00:00:00 2001 From: m4 Date: Tue, 11 Aug 2026 09:06:11 +0800 Subject: [PATCH] fix(webui): reject malformed JSON body in system config PUT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unparseable request body was silently converted to null, merged over defaults, and saved — wiping the admin's config while returning 200. Throw SystemConfigError on JSON parse failure so the route returns 400 INVALID_REQUEST without touching the saved config file. Co-Authored-By: Claude Opus 4.7 --- src/app/api/system/config/route.ts | 8 +++++++- src/app/api/system/config/routes.test.ts | 25 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/app/api/system/config/route.ts b/src/app/api/system/config/route.ts index aaf1ace..ff7dfef 100644 --- a/src/app/api/system/config/route.ts +++ b/src/app/api/system/config/route.ts @@ -5,6 +5,7 @@ import { NO_STORE, routeErrorResponse } from "@/lib/server/routeErrors"; import { getSystemConfig, saveSystemConfig, + SystemConfigError, validateSystemConfig, } from "@/lib/server/systemConfig"; @@ -39,7 +40,12 @@ export async function PUT(request: NextRequest) { } actor = requireActor(request); requireAdmin(actor); - const body = (await request.json().catch(() => null)) as unknown; + let body: unknown; + try { + body = await request.json(); + } catch { + throw new SystemConfigError("Invalid JSON body."); + } const config = validateSystemConfig(body); saveSystemConfig(config); return NextResponse.json(config, { headers: NO_STORE }); diff --git a/src/app/api/system/config/routes.test.ts b/src/app/api/system/config/routes.test.ts index 94282da..d6d6909 100644 --- a/src/app/api/system/config/routes.test.ts +++ b/src/app/api/system/config/routes.test.ts @@ -73,6 +73,31 @@ describe("system config routes", () => { expect(await response.json()).toMatchObject({ code: "INVALID_REQUEST" }); }); + it("PUT rejects an unparseable JSON body with 400 and does not overwrite the saved config", async () => { + const valid = await adminRoute.PUT( + request("http://localhost/api/system/config", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ branding: { wordmark: "KeepMe" } }), + }) as never + ); + expect(valid.status).toBe(200); + const before = fs.readFileSync(systemConfigPath(), "utf8"); + + const response = await adminRoute.PUT( + request("http://localhost/api/system/config", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: "not-json{", + }) as never + ); + expect(response.status).toBe(400); + expect(await response.json()).toMatchObject({ code: "INVALID_REQUEST" }); + + expect(fs.readFileSync(systemConfigPath(), "utf8")).toBe(before); + expect(JSON.parse(before)).toMatchObject({ branding: { wordmark: "KeepMe" } }); + }); + it("public route hides terms markdown when disabled", async () => { await adminRoute.PUT( request("http://localhost/api/system/config", {