github.com/example/demo-shop
Security findings
3 things here can be used against you as the code stands today. AI Review set aside one as a likely false positive.
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 now 3
ConfirmedCriticalAnyone 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 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
ConfirmedCriticalAnyone 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: 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
ConfirmedHighThe 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: 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
Likely false positives 1
AI Review read the code around these and doesn't think they're real. They're kept so you can check its reasoning — the severity shown is what it would be if the review is wrong.
Likely false positiveHigh if real Rotate if realThe value is a cache label, not a password lib/cache.ts:4Secret scanIf this is a real key, deleting it doesn't revoke it. It's still in git history, so treat it as public and rotate it with the provider.
A variable named like a key holds a long, random-looking value in a file that ships with the app. If it is a password or an API key, anyone who reads the code can use it.
AI Review: CACHE_KEY in lib/cache.ts is only joined onto cache entry names, so one deploy does not read another deploy's cache. It is never sent to an API or used to sign anything, so knowing it unlocks nothing. The scanner flags any key-named variable with a random-looking value, which is why it came up.
Found by Secret scan
AI Review
Reviewed all 4 findings from this scan. Three are real and independent — none share a root cause, so fixing one will not fix another. One is a likely false positive: the scanner's own severity would have put it beside the other three.
Details on each finding below.