fix: sanitize v1 API pagination inputs and validate growth date

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:27:27 +02:00
parent 9e32766046
commit 0a12cdfbcc
3 changed files with 21 additions and 3 deletions
+18 -1
View File
@@ -12,7 +12,7 @@
|----------|-------|-------| |----------|-------|-------|
| CRITICAL | 9 | 9 | | CRITICAL | 9 | 9 |
| HIGH | 11 | 10 | | HIGH | 11 | 10 |
| MEDIUM | 7 | 3 | | MEDIUM | 7 | 5 |
| LOW | 1 | 0 | | LOW | 1 | 0 |
_Updated after second-pass audit (pass 2 of 2)._ _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 ## Routes verified as already secure
- `webhooks/route.ts` — familyId from session, no cross-family access possible - `webhooks/route.ts` — familyId from session, no cross-family access possible
+2 -2
View File
@@ -18,8 +18,8 @@ export async function GET(req: Request) {
const type = searchParams.get("type"); const type = searchParams.get("type");
const from = searchParams.get("from"); const from = searchParams.get("from");
const to = searchParams.get("to"); const to = searchParams.get("to");
const limit = Math.min(parseInt(searchParams.get("limit") ?? "50"), 500); const limit = Math.max(1, Math.min(parseInt(searchParams.get("limit") ?? "50") || 50, 500));
const offset = parseInt(searchParams.get("offset") ?? "0"); const offset = Math.max(0, parseInt(searchParams.get("offset") ?? "0") || 0);
if (!babyId) return NextResponse.json({ error: "babyId is required" }, { status: 400 }); if (!babyId) return NextResponse.json({ error: "babyId is required" }, { status: 400 });
+1
View File
@@ -33,6 +33,7 @@ export async function POST(req: Request) {
const { babyId, date, weight, height, headCirc, notes } = body; const { babyId, date, weight, height, headCirc, notes } = body;
if (!babyId || !date) return NextResponse.json({ error: "babyId and date are required" }, { status: 400 }); 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 } }); const baby = await prisma.baby.findFirst({ where: { id: babyId, familyId: ctx.familyId } });
if (!baby) return NextResponse.json({ error: "Baby not found" }, { status: 404 }); if (!baby) return NextResponse.json({ error: "Baby not found" }, { status: 404 });