RowShield
Guides

The dashboard says protected, but the table is still readable

The dashboard shows the shield, the flag, the word protected. Then a request with no credentials returns the table anyway, and the two facts refuse to fit together. They fit once you know the third fact the dashboard does not show: a policy whose condition is always true. Row Level Security is enabled, the machinery runs on every query — and the machinery has been instructed to grant everything.

This is the tautological policy, usually born as USING (true) during a debugging session when something needed to work immediately. It is among the most common ways a Supabase table leaks while every configuration view insists it is safe. This page shows what such a policy does mechanically, why protection indicators stay green beside it, the queries that find it, and the rewrite that ends the argument.

Rules that check this

What an always-true policy does, and why indicators stay green

Policies are grants layered onto a default denial: with RLS enabled, a role sees a row only if some permissive policy's condition holds for it. A condition of true holds for every row and every caller the policy names, so the effect equals having no policy at all — with one cruel difference. No policy leaves the setup visibly unfinished; USING (true) makes the configuration look finished, audited and green.

The usual origin story is benign. A preview renders empty, someone asks how to let the demo read its data, and the fastest answer is a permissive policy intended as scaffolding. Scaffolding in databases does not announce itself later; it becomes the load-bearing wall, because nothing distinguishes it visually from a real rule once the session that wrote it has closed.

Protection indicators read the flag and the presence of policies, and both are satisfied: RLS is enabled, a policy exists, the coverage looks complete. None of these readers evaluate what the policy actually grants. Configuration views answer "is the machinery switched on"; behaviour is "what does the machinery do", and those two questions diverge exactly here.

Credit where due: the built-in Security Advisor flags several constant-true forms and is worth running today. It examines state when invoked, though, and a placeholder policy installed after its last run sits undetected until someone opens the page again. The advisor is a good tool; continuous monitoring exists for the interval it cannot see.

Find the policy yourself

The catalog stores every policy's full condition text, so the search is direct: list the policies on public tables and read the qual column for conditions that cannot fail. Constant-true shapes include true itself, 1 = 1, NOT false, and any OR arm built from those — RowShield normalises the deparsed expression to catch each of them.

SELECT schemaname, tablename, policyname, cmd, roles, qual
FROM pg_catalog.pg_policies
WHERE schemaname = 'public'
ORDER BY tablename, policyname;

Prove it, then rewrite it

Behaviour closes the argument. The request below uses only the public anon key — the same access any visitor already has — and a table behind a tautological policy answers it with rows despite every indicator saying protected. Substitute your hostname and a real table name, and prefer the table the dashboard vouches for loudest:

curl -s "https://your-project.supabase.co/rest/v1/orders?select=*&limit=5" \
  -H "apikey: $SUPABASE_ANON_KEY" \
  -H "Authorization: Bearer $SUPABASE_ANON_KEY"

The rewrite that aligns flag and behaviour

Rows confirm the diagnosis. The rewrite replaces the constant with the real condition — who owns each row in your schema — keeping RLS enabled throughout, so the indicator finally describes reality. Apply enablement and the honest policy in one change, then re-run the request above and expect refusal:

ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.orders FORCE ROW LEVEL SECURITY;

CREATE POLICY "orders_owner_read"
  ON public.orders
  FOR SELECT
  TO authenticated
  USING ((SELECT auth.uid()) = user_id);

Keeping the flag and the behaviour aligned

Two look-alikes deserve deliberate exemption, and honest tools ignore them on purpose. A RESTRICTIVE policy narrows whatever else applies, so USING (true) on one grants nothing by itself; and a policy scoped entirely to service_role decorates nothing, because that role bypasses RLS regardless. Reporting either would be noise, and suppressing them is part of what keeps a first scan believable.

RowShield detects tautological policies as their own rule, RLS_TAUTOLOGY, by normalising the deparsed condition and recognising the constant-true family — including OR-arm cases hiding behind one honest clause. Findings arrive with remediation generated from your actual columns, and scans diff between runs, so a placeholder that sneaks back raises a regression alert instead of passing unnoticed.

RowShield is an independent product, unaffiliated with and not endorsed by Supabase; it reads pg_catalog and bucket metadata only, never table rows, and references to Supabase and Postgres are descriptive. Run a free audit — no account — at rowshield.dev/audit, and see which of your tables behave differently from how the dashboard describes them.

Frequently asked

Is USING (true) ever legitimate?
Occasionally, and only deliberately: a genuinely public lookup table, or a RESTRICTIVE policy, which can only narrow other policies and grants nothing on its own. Outside those cases a constant-true condition is scaffolding that became the security model.
Should I rotate my keys after finding one?
No. The anon key behaved as designed and no credential needs replacing; the flaw is a policy granting too much. Rewrite the condition, verify with an unauthenticated request, and keep the keys where they are.
Why did the advisor not catch mine?
The advisor recognises many constant-true forms and is worth consulting, but it reports state when opened. A placeholder installed after its last run waits invisibly, which is precisely the gap continuous scanning exists to close.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
supabase protected but readableusing true policy supabasetautological rls policyrls enabled but table readable