DEV Community

Cenk KURTOĞLU
Cenk KURTOĞLU

Posted on Originally published at github.com

Supabase RLS returns rows it shouldn't (or throws 42501): the 6 real causes

If your Supabase app either shows data it shouldn't or blocks users with
42501 - new row violates row-level security policy, it's almost always one of
six causes. Work down the list; each has a 10-second check.

1. RLS was never enabled (the AI-tool trap)

Supabase enables RLS automatically only for tables created in the Table
Editor
. Tables created via SQL editor, migrations, Prisma, or an AI coding
agent ship wide open.

select relname, relrowsecurity
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where nspname = 'public' and relkind = 'r' and not relrowsecurity;
Enter fullscreen mode Exit fullscreen mode

Every row of that result is reachable by anyone holding your public anon key.

2. RLS enabled, but no policy matches ? everything denied

Postgres defaults to deny. If your table suddenly returns empty arrays after
you "turned security on", this is why: relrowsecurity = true with zero
applicable policies denies all rows to all roles.

3. The USING (true) policy that looks like security

create policy "allow_read" on public.profiles for select using (true);
Enter fullscreen mode Exit fullscreen mode

This satisfies "we have RLS" while exposing every row. Search your policies:

select tablename, policyname from pg_policies
where schemame='public' and (qual = '(true)' or with_check = '(true)');
Enter fullscreen mode Exit fullscreen mode

Each hit needs a human decision: is this table meant to be public?

4. Missing WITH CHECK on writes

A SELECT-only mindset leaks into INSERT/UPDATE policies without a WITH
CHECK
clause - and Postgres then derives an implicit check from USING,
which fails in confusing ways (especially on UPDATEs that rewrite the whole
row). Always write both halves explicitly:

create policy "users update own rows"
on public.items for update to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
Enter fullscreen mode Exit fullscreen mode

5. Dependent-policy lockout ("my admin policy is perfect but the table is empty")

Policies that subquery another table (exists (select 1 from profiles where ...))
fail silently when the inner table's own RLS hides its rows from the calling
user. Your admin sees nothing; a bare true policy "fixes" it - which is how
teams get talked into cause #3. Give the inner table a proper read-own-row
policy first.

6. Views, SECURITY DEFINER functions, and Storage

  • Post-15 views run as their owner by default ? can expose rows the table policies hide. Use security_invoker = true views.
  • security definer functions bypass RLS unless they set it themselves; pin set search_path.
  • Storage buckets have their own storage.objects policies - a private DB behind a public bucket is still public.

Prove your whole schema in 60 seconds: I put nine read-only queries that
cover causes 1-6 into a free SQL file you run in your own SQL editor - nothing
leaves your database:
github.com/cekuu35/supabase-rls-leak-demo ? audit/rls-audit.sql

The same repo contains a red/green test suite demonstrating the leak and the
one-file fix. If you want the full guided workflow (write-side checks,
role-simulation harness, remediation templates), that's the
Supabase RLS Audit Kit

  • $29, runs entirely against your own catalogs.

Disclosure: both links are mine. The audit queries and demo are free and MIT.

Top comments (0)