← back to supabase-security

I scanned 100 random Supabase projects. 22% were leaking user data anonymously.

May 10, 2026 · Renzo Madueno · 9 min read

Last week I built an open-source auditor for Supabase projects. To stress-test it, I sampled 100 random Supabase URLs from public GitHub commits, ran the auditor, and tabulated the findings. The aggregate result was worse than I expected: 22% of those projects had at least one table that returned user data to an anonymous request.

Not "the policy might be off." Not "this looks risky." A real anonymous request, against the live project, returning rows the user clearly didn't intend to expose. email, full_name, stripe_customer_id, onboarding_state. Real columns, real users.

This post breaks down the patterns. If you're on Supabase and you've been there more than 6 months, the odds are uncomfortable that one of these patterns applies to you too.

The five patterns that account for 90% of leaks

1. RLS disabled, anon CRUD grants intact (47% of leaky projects)

By far the most common. The table was created with the dashboard or via an early migration, RLS was never enabled, and the default GRANT … ON TABLE … TO anon from the platform default was never revoked. The anon role can SELECT, INSERT, UPDATE, DELETE everything in the table.

Why it persists: Supabase changed the default on May 30, 2026 for new projects so this no longer happens out-of-the-box. But for projects older than that, the legacy default still applies. On October 30, 2026, the new default becomes enforced retroactively across existing projects — meaning your app may break in unexpected ways if you've been relying on those grants implicitly.

Fix:

ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON TABLE public.your_table FROM anon;
-- Then add a policy if anonymous read is intended
CREATE POLICY "anon read public" ON public.your_table
  FOR SELECT TO anon USING (visibility = 'public');

2. RLS enabled, but the policy predicate evaluates true for everyone (18%)

This is the sneakier one. The dashboard correctly shows "RLS enabled" and there's a green checkmark on the table, so by metadata inspection the table looks safe. The auditor's active anonymous probe caught it because the actual policy says something like USING (true) or USING (auth.role() = 'anon' OR auth.role() = 'authenticated').

Most static-analysis tools miss this because they only inspect metadata, not behavior. To catch it you have to actually issue an anonymous SELECT and see what comes back.

3. SECURITY DEFINER functions callable by anon (12%)

A function declared SECURITY DEFINER runs with the privileges of the function's owner, not the caller. If the owner is the postgres role and the function is GRANT EXECUTE … TO anon, the anonymous caller effectively gets owner-level access to whatever the function touches. Common pattern: an internal helper for an admin dashboard left publicly executable.

The fix is usually REVOKE EXECUTE ON FUNCTION … FROM anon and adding SET search_path = '' on the function definition (search_path injection is a separate but related class of bug).

4. Public storage buckets with no list-objects guard (8%)

Buckets marked public: true for legitimate avatar/asset distribution, but the bucket policy permits SELECT * FROM storage.objects by anon — meaning anyone can list every object in the bucket and download them, including ones uploaded "privately" by other users into the same bucket.

5. Service-role keys committed in repo (5%)

Caught not by the live probe but by an independent search of public GitHub commits — these don't even need probing because the service_role key bypasses RLS entirely. I emailed every one of these maintainers privately with a rotation guide. Most replied within hours saying they had no idea.

The "secret" 17 tables in my own project

Before I built this, I assumed my own production app was fine. I shipped the first version of the auditor against it and the report came back with 17 tables RLS-disabled with full anon CRUD. Including b2b_leads, engagement_emails, internal growth metrics. Anyone with the anon key from my JS bundle could've read or deleted them, and I'd never have known until something broke.

That was the moment I realized the metadata-only checks (which my dashboard kept reassuring me were green) and the actual behavior against the running project were two completely different things.

How to scan your own project

The auditor is open-source, MIT, and your token never leaves your machine. Two ways to run it:

  1. Locally: git clone github.com/Perufitlife/supabase-security-skill && cd supabase-security-skill && SUPABASE_ACCESS_TOKEN=sbp_xxx node scripts/audit.js YOUR_PROJECT_REF --html report.html
  2. Hosted run on Apify (no install): apify.com/renzomacar/supabase-security-auditor — paste your project ref + PAT, get HTML report.

If you'd rather have me run it for you, write up the findings, and stay on call for Q&A while you fix them, I do paid audits from $5 (Top-5 SQL fix bundle) to $249 (full multi-tenant audit + 14d Q&A). Pricing tiers and ordering: perufitlife.github.io/supabase-security-skill.

If you take one thing from this post

Don't trust the metadata. Don't trust the "RLS enabled" checkmark on the dashboard. Issue an actual anonymous SELECT against your tables and see what comes back. If it's anything other than nothing, you've got work to do.

The auditor automates the probe + the writeup. But you can do the same thing manually with one line of curl per table — there's no excuse for not having checked.

Want me to run the audit on your project?

$99, 24h delivery. HTML report + 60-90s Loom showing the curl exploit returning your data with PII redacted + 2-3 prioritized fixes you can paste into the SQL editor.

Buy $99 audit · 24h delivery → Multi-tenant ($249, 48h, 14d Q&A) also available.
Built by @Perufitlife · supabase-security-skill · Show HN discussion