RLS Write-Leak: The Silent Data Exposure That Lets Any User Modify Any Row
You enabled RLS. SELECT is locked down — users only see their own rows. You tested it, it works.
But can any authenticated user INSERT/UPDATE/DELETE any row?
Most vibe-coded apps miss this. The dashboard shows green shields. pg_policies shows policies. But the write path is wide open.
The Write-Leak Pattern
-- Looks correct at first glance create policy "owner_read" on documents for select using (auth.uid() = user_id); -- Missing: INSERT, UPDATE, DELETE policies! -- Result: ANY authenticated user can write to ANY row Postgres RLS is per-command. A SELECT policy does nothing for INSERT/UPDATE/DELETE. If you don't explicitly policy-write operations, they fall through to grants — and GRANT INSERT ON documents TO authenticated is the default.
The 3 Silent Write-Leak Modes
| Leak Type | Policy Gap | Impact |
|---|---|---|
| Blind Insert | No INSERT policy + GRANT INSERT TO authenticated | Any user creates rows with any user_id — impersonation, data poisoning |
| Privilege Escalation | UPDATE policy only checks USING, missing WITH CHECK | User updates their row → changes user_id to victim's → now owns victim's data |
| Cascade Delete | No DELETE policy | Any user deletes any row — ransomware, data destruction |
Why This Survives Testing
// Your test (passes) const { data } = await supabase.from('documents').select() // Returns only your rows ✓ // Attacker's test (also passes - silently!) const { error } = await supabase.from('documents') .insert({ user_id: 'victim-id', content: 'pwned' }) // error = null, row created with victim's user_id The insert succeeds silently. No error. The row now belongs to the victim. Your app shows it to the victim. The attacker just poisoned their data.
The Fix: Complete Command Coverage
Every table needs all four commands covered:
-- 1. Enable RLS ALTER TABLE documents ENABLE ROW LEVEL SECURITY; -- 2. Owner: full access CREATE POLICY "owner_all" ON documents FOR ALL USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); -- 3. Org members: read only (multi-tenant) CREATE POLICY "org_read" ON documents FOR SELECT USING (org_id = current_org_id()); Critical: WITH CHECK on write policies. Without it, UPDATE can change the row to violate the USING condition post-write.
The 9-Catalog Audit (Run in SQL Editor)
-- 1. Tables missing RLS select c.relname from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind = 'r' and c.relrowsecurity = false; -- 2. Tables with SELECT policy but no INSERT policy select distinct p.polrelid::regclass as table_name from pg_policy p where p.polcmd = 'r' -- SELECT only and not exists ( select 1 from pg_policy p2 where p2.polrelid = p.polrelid and p2.polcmd in ('a','*') -- INSERT/ALL ); -- 3. Write policies missing WITH CHECK select p.polrelid::regclass, p.polname from pg_policy p where p.polcmd in ('a','*','w','u','d') -- write commands and p.polwithcheck is null; -- 4. Grants to authenticated without policies -- (run per table) Or use the free tool: github.com/cekuu35/supabase-rls-leak-demo — runs 47 checks including all write-leak vectors.
Real-World Impact
Case 1: SaaS with user-generated content
- Attacker inserts rows with
user_id = admin_id - Admin sees attacker's content in their dashboard
- XSS via stored content → full account takeover
Case 2: Multi-tenant B2B
- User in Org A updates
org_idto Org B's ID - Now sees/controls Org B's data
- No error, no audit log entry
Case 3: E-commerce orders
- Any user deletes any order
- Financial records destroyed
- No
DELETEpolicy = open season
The "Green Shield" Illusion
Supabase dashboard shows ✅ RLS Enabled + ✅ Policies Exist.
But it doesn't verify command coverage. A table with only SELECT policy gets the same green shield as a fully locked-down table.
Your 5-Minute Audit
# 1. Clone free tool git clone https://github.com/cekuu35/supabase-rls-leak-demo cd supabase-rls-leak-demo # 2. Run against your DB (read-only, safe) npm install npm run audit Output shows:
- ❌ Tables missing RLS
- ❌ Tables with SELECT-only policies
- ❌ Write policies missing WITH CHECK
- ❌ Grants without policies
- ❌ Service role key in client bundle
- ...and 40 more checks
CTA
Free: Run the audit → see your write-leaks
$29 Kit: 47-check checklist + remediation SQL for every finding
$50/mo Guard: CI gate that fails build on any RLS gap
Next in series: "4 Security Holes Your AI-Generated Supabase App Has" — the Lovable/Cursor/Bolt patterns that leak data by default.

