Disabled, no policies, always true: three different bugs
From the outside these three states wear the same face: a table exists, the app misbehaves or the data leaks, and the dashboard shows some flavour of green. Underneath they are different failures with different fixes, and applying the wrong one wastes an evening and teaches the wrong lesson.
RowShield is independent of Supabase and not endorsed by it; the mechanics below are simply how Postgres Row Level Security behaves.
RLS disabled entirely
relrowsecurity is false, so Postgres never consults policies because none apply. Any table in a PostgREST-exposed schema returns every row, readable and writable, to whoever presents the anon key. This is the shape most reported Supabase leaks take, which is why it leads the rule index at critical.
Detection is unambiguous: the flag on a developer-authored table. The finding also reports the column count and how many policies exist against the table, because policies drafted but never switched on is a common intermediate state worth naming. The fix is to switch RLS on, force it for the owner, and grant intended access with policies:
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY; ALTER TABLE public.documents FORCE ROW LEVEL SECURITY; -- Then add policies; enabling RLS with no policies denies everything.
RLS enabled with no policies
Here relrowsecurity is true and the policy table is empty. Postgres denies every row to every non-owner role, so nothing leaks, and equally nothing works: the client sees empty results and silent failures. Teams frequently discover this one weeks later through a support inbox rather than an incident report.
Server-side code often keeps working through exactly this failure, because the service role bypasses RLS entirely, so admin panels look fine while every client-facing feature starves. That asymmetry is why the breakage hides until real users exercise the affected screens.
The denial is structural, not behavioural: Postgres refuses rows because no policy grants them, which is the system working exactly as specified. The failure is configuration debt, and it announces itself as features that never quite worked. The fix is additive: leave RLS on and write the policies your access model implies, owner-scoped reads and writes being the usual shape. Remediation SQL on this finding proposes exactly those policies from your real columns.
A policy that always evaluates true
The subtlest of the three. RLS is enabled and a policy exists, so every advisor reports protection, yet the policy's USING clause is a constant true, which grants every row to every role it targets, the anon role included when targeting is public.
Tautologies usually arrive innocently: a predicate left as true to test with, or generated by a tool asked to switch protection on quickly. Because RLS reads enabled everywhere, the state survives audits, demonstrations and launches indefinitely. The fix is replacement, not addition: drop the permissive policy and recreate it with a real condition.
DROP POLICY documents_open ON public.documents; CREATE POLICY documents_select_own ON public.documents FOR SELECT TO authenticated USING (owner_id = (SELECT auth.uid()));
Telling them apart in thirty seconds
One query settles it: read relrowsecurity from pg_class, count policies in pg_policy, and inspect the expressions with pg_get_expr when a policy exists but looks suspicious. Or simply read the finding, which states which of the three it is, links the rule documentation and attaches the matching remediation.
The three states also fail in opposite directions, which is a useful sanity check when something feels wrong: disabled leaks loudly, no-policies starves quietly, and a tautology does neither until someone looks closely. A project showing all three on a first scan is ordinary, not doomed; fix in that order and the fourth scan looks boring.
SELECT n.nspname AS schema, c.relname AS table_name,
c.relrowsecurity AS rls_enabled,
count(p.polname) AS policies
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_catalog.pg_policy p ON p.polrelid = c.oid
WHERE n.nspname = 'public'
GROUP BY 1, 2, 3
ORDER BY 1, 2;Related questions
- Which of the three is most urgent?
- Disabled RLS and tautologies are both critical because rows leave; no-policies is high because nothing leaves but nothing works. Fix the two open ones first, then repair availability.
- Does enabling RLS break my app?
- Until policies exist, yes, by design: Postgres denies what no policy grants. Generate the owner policies first or in the same migration, then enable.
Did this answer your question? If not, tell us what is missing — article corrections go straight to the person who maintains it.