Phase 8: Security, Privacy, and Judgment Traps · 30 min · Supabase · Any vibe coding tool
Objective
Your mission
You're about to audit a real app for security holes that AI tools miss. By the end of this lesson, you'll have a completed security_audit.md that proves your app is safe to ship — or tells you exactly what to fix.
You need: Your deployed app (or one you're building), Supabase dashboard access, a terminal.
Time: 30 minutes of actual doing, not reading.
Step 1: check RLS on every table (5 min)
Open your Supabase dashboard → Table Editor.
For each table in your database, check: is RLS enabled?
Do this right now:
- List every table you have (e.g.,
habits, profiles, settings, completions) - For each one, check if RLS is enabled or disabled
- Write it down in your audit file:
## RLS Check
- habits: RLS enabled ✓
- profiles: RLS enabled ✓
- settings: RLS DISABLED ✗ — FIX NEEDED
Why this matters: Without RLS, anyone with your anon key (which is public — it's in your frontend code) can read and write ALL data in that table. One table without RLS = all data in that table is public. AI commonly enables RLS on the main table but forgets secondary tables.
If any table is missing RLS: Enable it now. Add a policy:
alter table settings enable row level security;
create policy "Users see own settings" on settings
for select using (auth.uid() = user_id);
create policy "Users update own settings" on settings
for update using (auth.uid() = user_id);
create policy "Users insert own settings" on settings
for insert with check (auth.uid() = user_id);
Adapt the policy to your table's columns. If your table doesn't have a user_id column, add one.
Step 2: hunt for the service_role key (3 min)
Open your terminal. Run:
grep -r "service_role" src/
grep -r "SERVICE_ROLE" src/
What you're looking for: Any mention of the service_role key in your frontend source code.
Why this matters: The service_role key bypasses ALL security. With it, anyone can read, modify, or delete everything in your database. It should NEVER appear in frontend code — only in server-side code (Edge Functions, serverless APIs).
Write it down:
## service_role Key Check
- grep result: [paste what you found, or "nothing found ✓"]
If you found it: Remove it from frontend code immediately. Only use the anon key in frontend. Rotate the key in Supabase (Settings → API → Reset service_role key) — it's compromised.
Step 3: test every form for input validation (5 min)
Open your app. Go to each form (signup, login, create habit, edit profile, etc.).
For each form, try these:
- Submit empty — does it accept blank input?
- Submit a very long string — paste 10,000 characters. Does it accept?
- Submit HTML — type
<script>alert('xss')</script> in a text field. Does it save? Does it render when displayed?
Write it down:
## Input Validation Check
- Signup form: validates email format ✓, requires password ✓
- Create habit form: accepts empty name ✗ — FIX NEEDED
- Habit name: no max length ✗ — FIX NEEDED
- HTML in habit name: renders as text (escaped) ✓
Why this matters: Without validation: empty input breaks the UI, excessively long input breaks layout and bloats the database, and HTML/JavaScript input is an XSS attack — the script runs when the name is displayed.
Fix: For each form that fails, add validation:
const name = formData.get('name')?.trim()
if (!name || name.length === 0) {
setError('Habit name is required')
return
}
if (name.length > 100) {
setError('Habit name must be 100 characters or less')
return
}
Step 4: verify auth rate limiting (2 min)
Go to Supabase Dashboard → Auth → Settings.
Check: are rate limits configured? (They are by default — but verify.)
Write it down:
## Rate Limiting Check
- Auth rate limits: configured ✓ (default Supabase settings)
Why this matters: Without rate limiting, an attacker can try thousands of passwords per second (brute force). Supabase has this built in, but if you built custom auth, you'd need your own.
Step 5: check CORS configuration (3 min)
Go to Supabase Dashboard → Settings → API.
Find the CORS configuration. What domains are allowed?
Write it down:
## CORS Check
- Allowed origins: localhost:5173, myapp.vercel.app ✓
- Wildcard (*) in production? No ✓
Why this matters: Without CORS restrictions, any website can make requests to your Supabase API. RLS still protects data, but CORS adds another layer — it prevents other sites from probing your API structure.
If CORS is set to * (wildcard) in production: Change it to only your domain(s). Add your production URL and localhost for development.
Step 6: search for SQL injection risk (2 min)
Run in your terminal:
grep -r "\.rpc(" src/
grep -r "raw sql\|rawSQL\|sql\`" src/
What you're looking for: Any raw SQL that concatenates user input.
Why this matters: String concatenation in SQL = injection attacks. The Supabase JavaScript client uses parameterized queries by default (you're safe). But supabase.rpc() or raw SQL can be vulnerable.
Write it down:
## SQL Injection Check
- No .rpc() calls found ✓
- No raw SQL concatenation ✓
- All queries use Supabase JS client (parameterized) ✓
Step 7: check for swallowed errors (5 min)
Run in your terminal:
grep -r "supabase.from" src/ --include="*.ts" --include="*.tsx" -A 2
For each database call you find, check: does it check for errors?
Bad (swallowed error):
await supabase.from('habits').insert(habit)
showToast('Saved!') // lies — might not have saved
Good (error checked):
const { data, error } = await supabase.from('habits').insert(habit)
if (error) {
showToast('Save failed: ' + error.message)
} else {
showToast('Saved!')
}
Write it down:
## Error Handling Check
- habits insert: error checked ✓
- habits select: error checked ✓
- settings update: NO error check ✗ — FIX NEEDED
- completions insert: NO error check ✗ — FIX NEEDED
Fix each one: Add const { data, error } = and if (error) to every database call that's missing it.
RLS is enabled on your habits table but not on settings. Why does that matter if the anon key is "safe"?
The anon key is public, shipped in your frontend. Any table without RLS is open to whoever holds it, and AI often forgets the secondary tables.