From 8b53a88073ae6c39b0626576fa359526b59cf378 Mon Sep 17 00:00:00 2001 From: RissRIce Date: Sat, 1 Aug 2026 02:08:43 -0600 Subject: [PATCH] fix(api-keys): reject invalid JSON bodies --- apps/web/app/api/account/api-keys/route.ts | 8 +++++-- apps/web/lib/api-key-request.test.ts | 28 ++++++++++++++++++++++ apps/web/lib/api-key-request.ts | 19 +++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 apps/web/lib/api-key-request.test.ts create mode 100644 apps/web/lib/api-key-request.ts diff --git a/apps/web/app/api/account/api-keys/route.ts b/apps/web/app/api/account/api-keys/route.ts index d9078d9..6dab4e4 100644 --- a/apps/web/app/api/account/api-keys/route.ts +++ b/apps/web/app/api/account/api-keys/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/session"; import { createApiKey, listApiKeys, revokeApiKey } from "@/lib/entitlements"; +import { readApiKeyRequest } from "@/lib/api-key-request"; export const runtime = "nodejs"; @@ -20,8 +21,11 @@ export async function POST(req: Request) { { status: 402 }, ); } - const body = await req.json().catch(() => ({})); - const created = await createApiKey(user.id, String(body.label || "API key")); + const body = await readApiKeyRequest(req); + if (!body.ok) { + return NextResponse.json({ ok: false, error: "Invalid request." }, { status: 400 }); + } + const created = await createApiKey(user.id, body.label); return NextResponse.json({ ok: true, apiKey: created.plaintext, prefix: created.prefix }); } diff --git a/apps/web/lib/api-key-request.test.ts b/apps/web/lib/api-key-request.test.ts new file mode 100644 index 0000000..b84283a --- /dev/null +++ b/apps/web/lib/api-key-request.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { readApiKeyRequest } from "./api-key-request"; + +function request(body: string): Request { + return new Request("https://aiornot.vote/api/account/api-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }); +} + +describe("readApiKeyRequest", () => { + it("rejects malformed JSON instead of creating a default key", async () => { + await expect(readApiKeyRequest(request('{"label":'))).resolves.toEqual({ ok: false }); + }); + + it("rejects JSON values that are not objects", async () => { + await expect(readApiKeyRequest(request("null"))).resolves.toEqual({ ok: false }); + await expect(readApiKeyRequest(request("[]"))).resolves.toEqual({ ok: false }); + }); + + it("uses the default label only for a valid JSON object", async () => { + await expect(readApiKeyRequest(request("{}"))).resolves.toEqual({ + ok: true, + label: "API key", + }); + }); +}); diff --git a/apps/web/lib/api-key-request.ts b/apps/web/lib/api-key-request.ts new file mode 100644 index 0000000..d4e16de --- /dev/null +++ b/apps/web/lib/api-key-request.ts @@ -0,0 +1,19 @@ +export type ApiKeyRequest = + | { ok: true; label: string } + | { ok: false }; + +export async function readApiKeyRequest(req: Request): Promise { + let body: unknown; + try { + body = await req.json(); + } catch { + return { ok: false }; + } + + if (!body || typeof body !== "object" || Array.isArray(body)) { + return { ok: false }; + } + + const label = (body as Record).label; + return { ok: true, label: String(label || "API key") }; +}