DEV Community

Cenk KURTOĞLU
Cenk KURTOĞLU

Posted on

RLS Write-Leak: The Silent Data Exposure That Lets Any User Modify Any Row

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)