From ebf93f9f22baa637ff6c7f6dc3fb907e1a0c7412 Mon Sep 17 00:00:00 2001 From: Vexx Date: Wed, 20 May 2026 23:42:31 +0530 Subject: [PATCH 1/2] fix: add strict input validation to POST /api/goals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrap req.json() in try/catch — returns 400 on malformed JSON - Validate body is a non-null object before destructuring - title: must be non-empty string, trimmed, max 100 characters - target: must be integer in [1, 10000] — blocks 0, negatives, floats, NaN, overflow - unit: clamped to 30 chars silently - recurrence: unknown values default to 'none' explicitly - All bounds defined as named constants Fixes division-by-zero (target:0 → Infinity% progress bar) and negative target making 0-progress goals show as 100% complete. Closes #454 --- .claude/settings.local.json | 21 -------------- src/app/api/goals/route.ts | 58 +++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 37 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index bf296420..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(npm --version)", - "Bash(psql --version)", - "Bash(gh pr:*)", - "Bash(gh issue:*)", - "Bash(/api/auth/callback/github)", - "Bash(gh label:*)", - "Bash(gh repo:*)", - "Bash(metricsController.ts)", - "Bash(git pull:*)", - "Bash(git stash:*)", - "Bash(git push:*)", - "Bash(gh release:*)", - "Bash(git rm:*)" - ] - } -} diff --git a/src/app/api/goals/route.ts b/src/app/api/goals/route.ts index 0a8eb0cd..aa198624 100644 --- a/src/app/api/goals/route.ts +++ b/src/app/api/goals/route.ts @@ -19,6 +19,12 @@ interface Goal { type Recurrence = "none" | "weekly" | "monthly"; +const VALID_RECURRENCES = ["none", "weekly", "monthly"] as const; +const MAX_TITLE_LEN = 100; +const MAX_UNIT_LEN = 30; +const MIN_TARGET = 1; +const MAX_TARGET = 10_000; + function getPeriodStart(recurrence: Recurrence): string { const now = new Date(); if (recurrence === "weekly") { @@ -106,21 +112,41 @@ export async function POST(req: Request) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } - const body = (await req.json()) as { - title?: string; - target?: number; - unit?: string; - recurrence?: Recurrence; - }; + let body: unknown; + try { + body = await req.json(); + } catch { + return Response.json({ error: "Invalid JSON" }, { status: 400 }); + } - if (!body.title || !body.target) { - return Response.json({ error: "title and target required" }, { status: 400 }); + if (typeof body !== "object" || body === null) { + return Response.json({ error: "Invalid request body" }, { status: 400 }); } - const recurrence: Recurrence = body.recurrence ?? "none"; - if (!["none", "weekly", "monthly"].includes(recurrence)) { - return Response.json({ error: "Invalid recurrence value" }, { status: 400 }); + const { title, target, unit, recurrence } = body as Record; + + if (typeof title !== "string" || title.trim().length === 0) { + return Response.json({ error: "title must be a non-empty string" }, { status: 400 }); } + if (title.length > MAX_TITLE_LEN) { + return Response.json({ error: `title must be ${MAX_TITLE_LEN} characters or fewer` }, { status: 400 }); + } + if ( + typeof target !== "number" || + !Number.isInteger(target) || + target < MIN_TARGET || + target > MAX_TARGET + ) { + return Response.json( + { error: `target must be an integer between ${MIN_TARGET} and ${MAX_TARGET}` }, + { status: 400 } + ); + } + + const safeUnit = typeof unit === "string" ? unit.slice(0, MAX_UNIT_LEN) : "commits"; + const safeRecurrence: Recurrence = VALID_RECURRENCES.includes(recurrence as Recurrence) + ? (recurrence as Recurrence) + : "none"; const user = await resolveAppUser(session.githubId, session.githubLogin); if (!user) return Response.json({ error: "User not found" }, { status: 404 }); @@ -129,11 +155,11 @@ export async function POST(req: Request) { .from("goals") .insert({ user_id: user.id, - title: body.title, - target: body.target, - unit: body.unit ?? "commits", - recurrence, - period_start: getPeriodStart(recurrence), + title: title.trim(), + target, + unit: safeUnit, + recurrence: safeRecurrence, + period_start: getPeriodStart(safeRecurrence), current: 0, }) .select() From 423d561ae8b24bbd35d346b4a55cbc778e9b0948 Mon Sep 17 00:00:00 2001 From: Vexx Date: Wed, 20 May 2026 23:43:46 +0530 Subject: [PATCH 2/2] chore: ignore .claude/ directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 236cc8a3..d7a7de4b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ Thumbs.db .idea/ *.swp +# Claude Code local settings +.claude/ + desktop.ini