Originally published on cenkkurtoglu.com.
You asked your AI to "add row-level security," the dashboard shows RLS enabled on every table, and you shipped. But "RLS enabled" and "RLS correct" are two different things — and the gap between them is where real user data leaks. I have found and helped fix these in production apps (two written up publicly, both maintainers said thanks in writing).
Here are seven surfaces, each with a read-only check you can paste into the Supabase SQL editor in 2 seconds, and the fix.
1. A policy with USING (true)
RLS is on, but the policy waves everyone through.
select schemaname, tablename, policyname, qual
from pg_policies
where schemaname = 'public' and qual = 'true';
Any row here is a table anyone (including anon) can read. Fix: scope it, e.g. using (auth.uid() = user_id).
2. A policy with no TO clause
No TO means it applies to every role, including anon. The single most common real leak I find.
select tablename, policyname, roles
from pg_policies
where schemaname = 'public' and roles = '{public}';
A value of {public} means it also applies to anonymous visitors. Fix: add to authenticated.
3. A permissive policy silently cancelling a strict one
Postgres OR-combines permissive policies. One USING (true) next to your careful auth.uid() = user_id means the loose one wins, for everyone. Fix: audit each table's full policy set together, never one at a time.
4. A SECURITY DEFINER function callable by anon
Functions run with the owner's rights and bypass RLS.
select p.proname
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef
and has_function_privilege('anon', p.oid, 'execute');
Fix: revoke execute ... from anon, public; and grant only to the role that needs it.
5. Policies on a table where RLS is off
The policies exist and look right, but if RLS was never enabled, none of them are enforced.
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 not c.relrowsecurity;
Cross-reference against tables that have policies. Fix: alter table ... enable row level security;
6. The service_role key in your browser bundle
A NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY, or the admin client imported into a client component, ships the RLS-bypassing key inside your JavaScript.
npm run build && grep -ro "service_role" .next/static
Any hit is critical. Fix: move it server-side (import 'server-only') and rotate the key — the old one is already published.
7. Storage and Realtime that table RLS does not cover
Your messages table is locked down, but the Storage bucket is public, or the Realtime publication broadcasts row changes to subscribers who should not see them. Fix: check bucket policies and your supabase_realtime publication separately from table RLS.
Run those seven. If they all come back clean, you are ahead of most shipped Supabase apps.
Want the full version — 60 checks, plus a role-simulation harness that queries your database as anon and as a second user to prove isolation? I packaged it as the Supabase RLS Audit Kit ($29). There is also a free read-only demo of the leak and its fix that runs in ~2s with no Docker: github.com/cekuu35/supabase-rls-leak-demo.
Top comments (0)