github.com/example/demo-shop
Don't ship yet
3 things here can be used against you as the code stands today.
Paste it into the assistant you built this with. It has your code open — the prompt carries what to change and how to check it worked.
Fix before launch 3
CriticalAnyone signed in can read every user's data supabase/migrations/0002_profiles.sql:12Code scanThe access rule on the profiles table allows any logged-in account to select any row. Someone signs up normally, opens the browser console, and reads every email address, phone number and Stripe customer id the app holds. No exploit needed — this is the database doing what it was told.
AI Review: Confirmed — Confirmed against the migration that created the policy: `USING (true)` matches every row, so any authenticated user can select any other user's profile.
Fix promptIn supabase/migrations/0002_profiles.sql, drop the permissive select policy on `profiles` and create one restricted to the owner (auth.uid() = user_id). Constraint: Leave the insert and update policies alone; do not disable RLS on the table to make anything pass. Verify: Sign in as a second user and assert they cannot select the first user's row.
Found by Code scan ·
rls-permissive-selectHighThe admin check only runs in the browser app/api/admin/users/route.ts:8Code scanThe dashboard hides admin buttons from normal users, but the API route behind them never checks who is calling. Anyone can call that endpoint directly with curl and do everything an admin can. Hiding a button is a UI decision, not a permission.
AI Review: Confirmed — No server-side authorization check exists anywhere under app/api/admin — the only admin check in the codebase is the conditional render in components/AdminPanel.tsx.
Fix promptAdd a server-side check at the top of every handler under app/api/admin that loads the session and returns 403 unless the user has the admin role. Constraint: Extract the check into one shared helper so a new route under app/api/admin cannot forget it. Verify: Add a test asserting a non-admin session gets 403 from every route under app/api/admin.
Found by Code scan ·
authz-client-onlyHighAnyone can fake a successful payment app/api/webhooks/stripe/route.ts:15Code scanThe Stripe webhook accepts any POST without verifying it came from Stripe. Someone can send a fake "payment succeeded" event and get a paid plan for free. This is the most common way small products lose money without noticing.
AI Review: Confirmed — `stripe.webhooks.constructEvent` is never called in this handler; the request body is trusted outright, so a forged "payment succeeded" event is indistinguishable from a real one.
Fix promptVerify the Stripe signature with stripe.webhooks.constructEvent using the raw request body and STRIPE_WEBHOOK_SECRET, returning 400 when verification fails. Constraint: Read the raw body via `await req.text()`, not `req.json()` — the Next.js App Router detail that makes the signature match at all. Verify: Add a test asserting an unsigned request is rejected with 400.
Found by Code scan ·
webhook-unverified
AI Review
Reviewed all 3 findings from this scan. All three are real and independent — none share a root cause, so fixing one will not fix another.
Reviewed with claude-opus-5. Details on each finding below.