Your Supabase RLS says "enabled." Here's why your database is still open.
- Green "RLS enabled" is not security. A policy of
USING(true)passes every dashboard check and returns every row to the public anon key. - A
curlwith your public frontend key and project URL pulls the entire table. No login, no auth. The anon key is in your_next/staticbundle; anyone who opens DevTools has it. - Check
pg_policiesdirectly. The deep-scan audit reads actual policy text and flagsUSING(true)as CRITICAL. Pair it with a CI scan so you catch a bad policy when it's merged, not when it ships.
The Supabase dashboard has a green "RLS enabled" indicator on every table. It's honest. It tells the truth. It also tells you almost nothing about whether your data is safe.
Row Level Security, quickly
Postgres supports per-row access rules. You enable it on a table with ALTER TABLE foo ENABLE ROW LEVEL SECURITY;. Then you write policies that define which rows each user can see. Without at least one policy, RLS-enabled tables reject everything by default — which is why most teams write a policy quickly just to make things work.
The fast way to "make things work" is to write a policy that allows everything:
CREATE POLICY "Anyone can read" ON items
FOR SELECT
USING (true);
The dashboard now shows: RLS ON, one policy, all green. The API docs generated by Supabase show a clean GET /rest/v1/items endpoint. The app works.
The database is also wide open to anyone with your anon key — which is public by design and shipped in every client-side bundle.
How bad is it?
Here's a probe anyone can run against a Supabase project, with only the public anon key (which is publicly visible in your frontend JS):
curl "https://<project>.supabase.co/rest/v1/items?select=*" \
-H "apikey: <anon_key>" \
-H "Authorization: Bearer <anon_key>"
If your policy is USING(true), this returns every row. Every email. Every user-submitted comment. Every message.
You don't have to be logged in. You don't have to be a user of the app. You just have to know the project URL and the public key — both of which are in the page source.
Where this shows up
We've encountered this pattern in three places, consistently:
- First migration after
supabase init. A developer creates amessagesoritemstable, the app breaks because RLS is blocking everything, and they write aUSING(true)policy to unblock themselves. It's meant as temporary. It rarely is. - Copy-pasted from Stack Overflow. The first answer that works usually works because it disables the check.
- "Public" tables that aren't actually public. A
poststable for a blog is fine to leave open. Auser_profilestable with emails is not. The policy is the same; the intent is different.
How we test it
Our Supabase provider runs two queries using your service_role key (stored only for the duration of the scan, never written to disk):
SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public'— identifies every table and whether RLS is on.SELECT * FROM pg_policies WHERE schemaname = 'public'— pulls every RLS policy and itsUSINGclause.
If we see a policy with USING(true) (or USING(1=1), USING(TRUE), or any other always-true expression), we flag it CRITICAL. If we see a table with rowsecurity=false, we flag it CRITICAL regardless of policies — there are no policies to check.
We also probe from the outside using only the anon key, as a second-source check: if the table returns rows to an unauthenticated client, something is wrong with the policy chain.
The honest fix
There's no clever shortcut. You have to write per-user policies:
CREATE POLICY "Users read their own items" ON items
FOR SELECT
USING (auth.uid() = user_id);
For genuinely public tables (the kind you'd put on a static page), it's better to serve them through a read-only view or a Supabase Edge Function with explicit allowlisting, rather than leaving a USING(true) policy lying around that someone else will later mistake for your private tables' policy.
Test your own project — and keep it tested
Run a deep scan with your Supabase credentials and we'll tell you exactly which policies look safe and which don't. The credentials never leave your scan session.
Then wire it into CI so the next migration that ships a USING(true) gets caught before merge, not by a stranger on a Saturday:
- GitHub Action — fails the PR check when a new permissive policy lands.
- GitLab CI template — same idea, native syntax.
Don't ship until you're sekrd
Run a free scan to find the vulnerabilities your AI missed.
Scan Your App Free