FROM THE VIDEO · FREE · ONE PAGE
3 checks.
+ the fixes.
Nobody hacked the apps in the video. They were asked, and they said yes. Run these on your own app tonight and make sure yours says no.
Visit your app as a stranger.
≈ 5 MINUTES · CATCHES THE OPEN DRAWER
Test
- Log out, or open a private window. You are now nobody.
- Using only the public key (the one in your site's code), ask for each table. Supabase example, for your own project:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/users?select=*&limit=5" \ -H "apikey: YOUR_ANON_KEY" - Repeat for every table, and open each storage bucket's file URLs while logged out.
- Supabase SQL editor, tables with security switched off (expect zero rows):
select tablename from pg_tables where schemaname = 'public' and rowsecurity = false;
PASS Empty results, or only rows that are meant to be public.
FAIL Any row a stranger shouldn't see. The drawer is open.
Fix
- Turn row level security on for every table.
- Add policies so each user only sees their own rows:
alter table bookings enable row level security; create policy "own rows" on bookings for all using (auth.uid() = user_id) with check (auth.uid() = user_id); - Do the same for storage buckets. On Firebase, replace test-mode rules with rules that check
request.auth. - Run the stranger test again.
Search your live site for secrets.
≈ 5 MINUTES · CATCHES THE MASTER KEY IN THE WINDOW
Test
- Open your live site → DevTools (F12) → Sources → search all files (Ctrl+Shift+F, Mac ⌘⌥F).
- Search, one at a time:
secret·service_role·sb_secret·sk_live·sk-·sk-ant·Bearer
PASS Only keys made to be public: Supabase anon / publishable, Stripe pk_, Firebase web config.
FAIL A Supabase service_role / secret key, a Stripe sk_, any OpenAI or Anthropic key, a database URL with a password.
Fix
- Move the call that needs the secret to a server route or edge function. The browser calls your route; only the server holds the key.
- Rotate the key in the provider's dashboard. Once it has been public, assume it has been copied.
- Check your git history too: a secret deleted from the code is still in old commits.
List every route that changes data.
≈ 15 MINUTES · CATCHES THE SIDE DOOR
Test
- Write down every route or server action that creates, changes or deletes something: signup, invites, payments, deletes, admin.
- Next to each, write what stops a stranger from calling it. On the server, not in the UI.
- Call each one logged out (or as a different user) against your own app and check you get 401 / 403.
| Route | What stops a stranger? |
|---|---|
POST /invite | must be logged in and an admin of that team ✓ |
DELETE /booking/:id | must own that booking ✓ |
POST /admin/role | "nobody knows the URL" ✗ not a lock |
Fix
- Check who is asking at the top of every handler, on the server:
const user = await getUser(req); // who is asking? if (!user) return res.status(401).end(); const booking = await db.booking.find(req.params.id); if (booking.ownerId !== user.id) return res.status(403).end(); // is it theirs? - Hidden buttons and secret URLs are not locks. Anything in the browser can be called directly.
YOU CHECKED 3. THERE ARE 15.
Find the rest in one command.
The Ship Safe kit: a scanner for your code and live site, a one-paste Supabase audit, and a field guide with a test, fix prompt and reference code for all 15 holes AI apps ship with.
Only run these checks against apps you own or have written permission to test.