fix(webui): reject malformed JSON body in system config PUT

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 <noreply@anthropic.com>
This commit is contained in:
m4
2026-08-11 09:06:11 +08:00
parent 3ddd953e1b
commit e8f09f240e
2 changed files with 32 additions and 1 deletions
+7 -1
View File
@@ -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 });
+25
View File
@@ -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", {