USING (true): the policy that protects nothing
This is the failure that survives an audit. RLS is enabled, so every indicator says protected. A policy exists, so the table looks governed. But the policy's condition is a constant true, and Postgres evaluates it honestly: every row passes for every role the policy targets.
It usually starts as scaffolding — USING (true) written to unblock development, meant to be narrowed later. Later is not enforced by anything, which is why it needs a detector instead of good intentions.
Rules that check this
- criticalPolicy always evaluates to true
RLS_TAUTOLOGY
What actually happens
A permissive policy whose USING expression is constant true contributes exactly what its text promises: unconditional access. For SELECT that means the whole table to the anon or authenticated role it targets. Postgres does not warn, because the statement is perfectly legal and sometimes even intentional for genuinely public data.
RowShield normalises the deparsed expression before judging, recognising several spellings of the same mistake: literal true, 1 = 1, NOT false, matching string literals, and OR arms containing those forms. A policy is only reported when the whole expression reduces to truth — an AND with a real condition stays a real condition.
The shape usually has a history. It begins as deliberate scaffolding during a demo or a migration dry-run, survives because nothing fails afterwards, and then spreads by copying: someone clones the invoices policy for refunds, edits the table name, and ships a second unconditional grant. Constant truth is contagious precisely because it never raises an error for anyone to investigate.
Two cases deliberately not flagged
Reporting noise is how detectors get muted, so two lookalikes are suppressed on purpose. A RESTRICTIVE policy with USING (true) grants nothing — RESTRICTive clauses can only narrow. And a policy scoped entirely to service_role is decoration, because that role carries BYPASSRLS anyway; the Supabase dashboard generates exactly that policy by default.
If your advisor shows a green shield next to such a table, this is why: the decorative policy satisfies naive checkers while bounding nothing.
One subtler lookalike resists automatic detection entirely: a permissive policy whose expression is true in practice rather than in logic — status = 'active' on a table where every row carries that value. Postgres sees a real predicate and so does the detector, which reports only literal reductions. Treat such policies as human review material: the catalog query lists them, and your knowledge of the data closes them.
Finding them yourself
Policies live in pg_policies. Eyeballing expressions works until you have forty tables; the query lists every permissive policy with its condition for review:
SELECT schemaname, tablename, policyname, roles, cmd, qual FROM pg_catalog.pg_policies WHERE schemaname = 'public' AND permissive = 'PERMISSIVE' ORDER BY tablename, policyname;
The fix
Replace the placeholder with the ownership predicate your schema implies, scoped to authenticated rather than anon where the data is user-private:
DROP POLICY "allow_all_reads" ON public.documents; CREATE POLICY "owners_read_documents" ON public.documents FOR SELECT TO authenticated USING ((SELECT auth.uid()) = owner_id);
Then watch for its return
Constant-true policies come back the way they left: someone re-scaffolds a feature, or an agent "simplifies" a failing query. RowShield treats the fix as a monitored state — resolved when narrowed, regressed if the placeholder reappears, alerting transitions only so a quiet project stays quiet.
Common false leads
Revoking grants feels related but addresses a different layer: with RLS enabled and a constant-true policy in place, the policy alone admits every row, so tightening grants changes nothing visible. Adding a second, stricter permissive policy narrows nothing either, because permissive policies combine with OR and the loose one keeps standing. The placeholder must be rewritten or dropped, not outvoted.
A green indicator elsewhere is another dead end. Naive checkers treat policy existence as protection, so tools that count policies rather than reading their expressions report this table as secured. When a scanner and your own reading disagree, trust the deparsed expression in pg_policies.qual — it shows the condition Postgres actually evaluates.
Verifying the fix from outside
Narrow the policy, then fetch the table with the anon key. Before the fix the response carries rows; after it you should receive an empty array or an error, and a request presenting a real user token should return exactly that user's rows and no one else's. Comparing the two responses takes a minute and proves the predicate binds callers, which catalog inspection alone cannot show.
Frequently asked
- Is USING (true) ever correct?
- For genuinely public data, yes: a marketing_pages table can be world-readable by design, and an explicit true documents that choice rather than hiding it. The finding is worth checking either way, because most constant-true policies were scaffolding rather than decisions — read the table name before dismissing the alert.
- Does wrapping auth.uid() matter here?
- Wrapping affects performance, not exposure: (SELECT auth.uid()) lets Postgres evaluate the function once per statement instead of once per row. Security-wise the wrapped and bare forms are identical — both compare the same value to the same column — so treat the wrap as a cost fix, never as a hardening step.
- Why did my scan not flag the service_role policy?
- Because such a policy grants nothing beyond what BYPASSRLS already allows: service_role skips policy evaluation regardless of what its policies say. Reporting decorative configurations would bury real findings under noise, so the detector excludes them deliberately and documents that exclusion alongside its other non-findings.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit