From 0a12cdfbcce06795d99f7c58e6e1d79f48465056 Mon Sep 17 00:00:00 2001 From: Arnaud Nelissen Date: Mon, 15 Jun 2026 23:27:27 +0200 Subject: [PATCH] fix: sanitize v1 API pagination inputs and validate growth date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - v1/events GET: offset now clamped ≥0, limit fallback prevents NaN being passed to Prisma take/skip - v1/growth POST: validate date is a valid ISO date before new Date() to avoid Invalid Date silently propagating into DB Co-Authored-By: Claude Sonnet 4.6 --- SECURITY_AUDIT.md | 19 ++++++++++++++++++- src/app/api/v1/events/route.ts | 4 ++-- src/app/api/v1/growth/route.ts | 1 + 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/SECURITY_AUDIT.md b/SECURITY_AUDIT.md index f5b9f92..734dd13 100644 --- a/SECURITY_AUDIT.md +++ b/SECURITY_AUDIT.md @@ -12,7 +12,7 @@ |----------|-------|-------| | CRITICAL | 9 | 9 | | HIGH | 11 | 10 | -| MEDIUM | 7 | 3 | +| MEDIUM | 7 | 5 | | LOW | 1 | 0 | _Updated after second-pass audit (pass 2 of 2)._ @@ -199,6 +199,23 @@ If `SUPERADMIN_EMAIL` env var is not set, the condition `SUPERADMIN_EMAIL && ses --- +### 23. `v1/events` GET — `offset` not sanitized (pass 2) +**Fix:** `Math.max(0, parseInt(offset) || 0)`. Also fixed `limit`: `Math.min` of `NaN` returns `NaN`; added `|| 50` fallback. + +--- + +### 24. `v1/growth` POST — date not validated before `new Date()` +**Fix:** Added `if (isNaN(new Date(date).getTime())) return 400`. + +--- + +## MEDIUM — Not applicable (already handled) + +### Upload route — extension from MIME type, not filename +The audit flagged `upload/route.ts` for unsafe extension extraction. The route derives the extension from `file.type` (e.g., `"image/jpeg".split("/")[1]`) — not from the original filename — and `ALLOWED_TYPES.includes(file.type)` is checked before that line. No fix needed. + +--- + ## Routes verified as already secure - `webhooks/route.ts` — familyId from session, no cross-family access possible diff --git a/src/app/api/v1/events/route.ts b/src/app/api/v1/events/route.ts index 21c771b..30030ff 100644 --- a/src/app/api/v1/events/route.ts +++ b/src/app/api/v1/events/route.ts @@ -18,8 +18,8 @@ export async function GET(req: Request) { const type = searchParams.get("type"); const from = searchParams.get("from"); const to = searchParams.get("to"); - const limit = Math.min(parseInt(searchParams.get("limit") ?? "50"), 500); - const offset = parseInt(searchParams.get("offset") ?? "0"); + const limit = Math.max(1, Math.min(parseInt(searchParams.get("limit") ?? "50") || 50, 500)); + const offset = Math.max(0, parseInt(searchParams.get("offset") ?? "0") || 0); if (!babyId) return NextResponse.json({ error: "babyId is required" }, { status: 400 }); diff --git a/src/app/api/v1/growth/route.ts b/src/app/api/v1/growth/route.ts index f43d53f..a56b76f 100644 --- a/src/app/api/v1/growth/route.ts +++ b/src/app/api/v1/growth/route.ts @@ -33,6 +33,7 @@ export async function POST(req: Request) { const { babyId, date, weight, height, headCirc, notes } = body; if (!babyId || !date) return NextResponse.json({ error: "babyId and date are required" }, { status: 400 }); + if (isNaN(new Date(date).getTime())) return NextResponse.json({ error: "Invalid date format" }, { status: 400 }); const baby = await prisma.baby.findFirst({ where: { id: babyId, familyId: ctx.familyId } }); if (!baby) return NextResponse.json({ error: "Baby not found" }, { status: 404 });