I audited my own Supabase project and found it wide open — with RLS on
I ran a security audit on my own Supabase project last week. RLS was enabled. Three policies per table. The dashboard showed everything green. Every table was wide open to anyone holding the publishable key. Here's how
I ran a security audit on my own Supabase project last week. RLS was enabled. Three policies per table. The dashboard showed everything green.
Every table was wide open to anyone holding the publishable key.
Here's how that happens, and the 10 checks I now run before every launch.
The trap
The Supabase Table Editor shows you data as postgres — which bypasses RLS entirely. So "I can see the rows in the dashboard, and there are 3 policies" tells you exactly nothing about what the anon role sees. I only found it by querying as anon directly:
set role anon;
select count(*) from agent_performance; -- every row. all of them.
The policies were real. They were also USING (true) — which means "everyone." RLS on ≠ secured.
It got worse on the second project
Different project, 10 tables with RLS disabled outright. The main work_orders table: readable, writable, deletable by anyone with the key. And it was wearing 4 policies that did absolutely nothing — because with RLS off, Postgres ignores every policy on the table. Decorative policies. The dashboard showed them; they enforced nothing.
The 10 checks
Paste the whole thing into the SQL Editor. Every query is read-only — nothing is created, granted, or altered. Any rows = a red flag.
1. RLS enabled on every public table
select tablename from pg_tables
where schemaname = 'public' and rowsecurity = false;
2. No table with RLS on but zero policies
select tablename from pg_tables t
where schemaname = 'public' and rowsecurity = true
and not exists (select 1 from pg_policies p
where p.schemaname = 'public' and p.tablename = t.tablename);
3. No USING (true) / WITH CHECK (true) policies
select tablename, policyname, cmd from pg_policies
where schemaname = 'public'
and (qual = 'true' or with_check = 'true');
4. Every INSERT/UPDATE policy has a WITH CHECK
select tablename, policyname from pg_policies
where schemaname = 'public' and cmd in ('INSERT','UPDATE','ALL')
and with_check is null;
5. No policies on tables with RLS disabled (decorative)
select p.tablename, p.policyname from pg_policies p
join pg_tables t on t.schemaname = p.schemaname and t.tablename = p.tablename
where p.schemaname = 'public' and t.rowsecurity = false;
6. No SECURITY DEFINER functions without pinned search_path
select p.proname as function_name from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef = true
and (p.proconfig is null or not (
select bool_or(split_part(c, '=', 1) = 'search_path')
from unnest(p.proconfig) as c));
7. No SECURITY DEFINER views in public
select c.relname as view_name from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v'
and c.reloptions::text ilike '%security_definer%';
8. anon can't reach what it shouldn't (spot-check your main tables)
-- run as anon in the SQL Editor, or curl with your anon key:
-- curl "https://<ref>.supabase.co/rest/v1/<table>?select=*" -H "apikey: <ANON_KEY>"
-- If it returns rows you expected to be private, RLS isn't doing its job.
9. Oct 30 ready: tables have Data API grants
select tablename from pg_tables t
where schemaname = 'public'
and not has_table_privilege('anon', format('public.%I', tablename), 'SELECT');
-- ^ lists tables anon can't touch at all; if your app needs one, grant it before Oct 30
10. Default privileges set for future tables
select count(*) from pg_default_acl d
join pg_namespace n on n.oid = d.defaclnamespace
where n.nspname = 'public';
-- 0 rows = every table created after today needs a manual GRANT. Set it once.
Two things I'd do differently
1. Policies first, RLS second, tighten third. The naive fix for "RLS off everywhere" is turning it on — which instantly locks out your own app, because RLS-on with zero policies means zero rows for everyone. Write the policies first, enable RLS, then narrow them.
2. Map every policy to its feature before touching it. My first instinct was dropping the anon write policies. That would have killed the CSV upload, which writes to Supabase as part of the flow. Security fixes that break the product get reverted. Document the tradeoff instead.
Oct 30
Supabase stops auto-exposing new tables to the Data API on Oct 30, 2026 — new tables without explicit grants 42501. Check 9 above tells you if you're ready. (It's a grants change, not an RLS change — your policies work exactly the same.)
If Supabase's final-notice email landed in your inbox this week, this is the change it's warning you about.
The full kit
Everything above runs free in your SQL editor — that's the whole point. Locked Down is for when you want the write-ups, the fix playbook, and the two full audits with the wrong calls included: $29 until Oct 30: https://tavora2.gumroad.com/l/lsoerp
Scope note: this is a review aid for projects you own or are authorized to test — not a penetration test, not a certification.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.