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_id to 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 DELETE policy = 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.