1. Object-level authorisation — FAIL
app/api/invoices/[id]/route.ts:14 fetches by id with no owner check. Any signed-in user can read any invoice by changing the id.
2. Secrets — FAIL
NEXT_PUBLIC_STRIPE_SECRET_KEY in .env.local is shipped to the client bundle; the secret key must not have the NEXT_PUBLIC_ prefix.
3. Server-side enforcement — FAIL
The "3 free uploads" limit is checked in components/Uploader.tsx only; the /api/upload route accepts unlimited calls.
4. Input handling — PASS (parameterised queries via Supabase client; uploads restricted to PDF under 10 MB server-side)
5. Auth plumbing — CANNOT TELL — no rate limit visible on /api/auth/magic-link; confirm at the provider.
6. Webhooks — PASS — stripe.webhooks.constructEvent verifies the signature before any write.
7. Data exposure — FAIL — /api/me returns the full users row including stripe_customer_id and internal notes.
8. Dependencies — PASS — no critical advisories in lockfile.
Ranked by exploitability: #1 (no account needed beyond free sign-up), #3 (free plan abuse), #2 (key visible in view-source), #7.
Fix for #1:
- const invoice = await db.from("invoices").select("*").eq("id", id).single()
+ const invoice = await db.from("invoices").select("*").eq("id", id).eq("user_id", session.user.id).single()- Claude Code
- Codex
- Copilot
- +2
Pre-Launch Security Review
An eight-point check for the mistakes that get small apps breached in month one, with PASS/FAIL per item.
