The USING(true) Trap: RLS That Passes the Scan but Leaks Everything

One of the worst Supabase RLS mistakes: a USING(true) policy looks enabled but leaks every row. Learn why AI tools generate it and how to rewrite it safely.

Barret8 min read

You did the responsible thing. You heard that Supabase tables need Row Level Security, you checked your dashboard, and every table showed RLS enabled. Maybe a scanner even gave you a green checkmark. You exhaled.

There's a version of "RLS enabled" that still leaks every row of your database to anyone who asks. It's one of the most common and most dangerous Supabase RLS mistakes, and it slips past both the dashboard and most scanners, because on paper everything looks correct. The policy is there. The switch is on. And the data pours out anyway.

The culprit is a policy written as USING (true). This post explains what that means, why AI builders generate it constantly, why a naive scanner calls it safe, and how to rewrite it into a policy that protects your users — with before-and-after SQL you can copy.

⚡ TL;DR

  • A policy of USING (true) matches every row, so RLS is technically enabled but protecting nothing. Your data is fully readable by anyone with the public anon key.
  • AI builders generate USING (true) because it makes the app "work" in the preview without anyone logging in.
  • A scanner that only checks "is RLS enabled?" reports safe and misses this completely. The only reliable test is checking the rows actually returned to a stranger.
  • The fix is one line: replace using (true) with using ( auth.uid() = user_id ).

What USING(true) actually means

An RLS policy has a condition called USING. Think of it as the question the database asks about each row: "Should this person be allowed to see this row?" If the answer is yes, the row is returned; if no, it's hidden.

A good policy makes that question specific. For example:

using ( auth.uid() = user_id )

This asks, "Is the logged-in user's ID equal to the user_id stored on this row?" Only rows the user owns pass. Everyone else's rows stay hidden.

Now look at the trap:

using ( true )

This asks nothing. true is always true, for every row, for every user, logged in or not. So every row passes the check. RLS is switched on, a policy exists, and yet the policy grants universal access. It's a lock that's been installed and then propped permanently open.

The result: a table with USING (true) is exactly as exposed as a table with RLS turned off. Both hand the whole table to anyone holding the anon key — which is public by design and sitting in your frontend for anyone to grab.

Why AI tools generate USING(true)

This isn't a rare accident. It's a predictable side effect of how AI builders work.

When you ask an AI tool to "add a database so users can save their notes," it optimizes for one thing: making the feature work in the preview. And in the preview, there's often no logged-in user — the session is anonymous. A strict policy like auth.uid() = user_id would return nothing there, and the feature would look broken.

So the AI does the thing that makes the demo work: it writes a policy that lets everything through. USING (true) makes the notes appear instantly. The app looks finished, and because it technically did enable RLS and did write a policy, the work looks careful and complete.

It isn't. The tool traded your security for a working preview, and unless you read the policy text, you'd never know. This is the same root cause behind the most documented Supabase incident there is.

This is the CVE-2025-48757 pattern

If this feels theoretical, it isn't. It's the exact failure behind the first published CVE for this category.

CVE-2025-48757, disclosed in May 2025, came from a scan of 1,645 Lovable-built apps. It found 170 of them — about 10% — leaking real user data across 303 endpoints. The leaks ran through the public anon key against tables with missing or USING(true) RLS policies. The same pattern has since appeared in apps built on Bolt, v0, Cursor, and Replit.

In other words, USING (true) isn't a fringe mistake — it's half of the most-documented vibe-coding vulnerability in existence. We break down the whole incident in the CVE-2025-48757 breakdown.

Why a naive scanner says "safe"

Here's the part that should make you skeptical of any tool that gave your app a clean bill of health.

A lot of scanners check a single thing about your tables: is RLS enabled, yes or no? That's an easy property to read — it's a boolean in the database metadata. If it comes back "yes," the scanner prints a reassuring green result and moves on.

But "RLS enabled" and "RLS protecting you" are different facts. A USING (true) policy makes the first true and the second false. A scanner that stops at the switch tells you you're safe while a stranger reads your entire user table. It's checking whether the lock is installed, not whether it's locked.

⚠️ Watch out

"RLS enabled" is not the same as "your data is protected." A scanner — or a dashboard — that only reports whether RLS is turned on will miss a USING (true) policy entirely. If a tool gave you a green check based on the switch alone, it didn't actually test whether your rows are reachable.

This is the whole reason the brand exists. Is My Site Hackable? doesn't stop at reading the switch. It queries your live endpoints as an anonymous user and looks at the rows that actually come back. A USING (true) policy returns real data to that request, so it gets caught — the same way an attacker would catch it. Reading metadata is easy; testing reality is the point.

How to check your own app

There are two ways to find this in your project. Do both if you can.

1. Read the policy text. In the Supabase dashboard, go to Authentication → Policies and click into each table's policies. Look at the USING expression. If you see USING (true) — or USING (1=1), or any always-true condition — that policy is not protecting the table.

2. Test as a stranger (the reliable check). Query your own REST endpoint with nothing but the public anon key and see what returns:

curl "https://YOUR-PROJECT.supabase.co/rest/v1/notes?select=*" \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"

If this returns an empty list [] or a permission error, the table is protected. If it returns real rows — even though the dashboard says RLS is enabled — you've got a USING (true) style policy leaking the table. The endpoint test is the truth; the switch is only a label. For the full method across anon, owner, and other-user, see testing RLS the right way.

Above all, don't trust a green checkmark based only on whether RLS is on. Trust the rows.

Worried a "passing" scan missed a USING(true) leak?

Run a free, read-only scan of your live app. Is My Site Hackable? checks the rows your endpoints actually return — so an enabled-but-leaking policy gets caught, not waved through.

Scan my app free →

How to fix it: before and after

The fix is short. You replace the always-true condition with one that ties each row to its owner. Here's the dangerous version and the safe version side by side.

Before — leaks everything:

-- RLS is "enabled," but this policy passes every row to everyone
create policy "Public read"
on notes for select
using ( true );

After — restricts rows to their owner:

-- Drop the leaky policy
drop policy "Public read" on notes;

-- Make sure RLS is on
alter table notes enable row level security;

-- A user can read only their own rows
create policy "Users read own notes"
on notes for select
using ( auth.uid() = user_id );

-- A user can write only their own rows
create policy "Users write own notes"
on notes for insert
with check ( auth.uid() = user_id );

The one line that matters most is using ( auth.uid() = user_id ). That's the real filter — it lets the database return a row only when the logged-in user owns it. After you apply this, run the stranger test again. An anonymous request should now come back empty.

If you're working through an AI builder, you can hand it this instruction directly:

Audit every RLS policy on my tables. For any policy that uses USING(true)
or any always-true condition, replace it with a policy that restricts rows
to their owner: using ( auth.uid() = user_id ). Then confirm an anonymous
request to each table returns no rows.

For the foundational explanation of how these policies work, start with Supabase RLS explained. For the wider set of Supabase risks, see the complete guide to Supabase security for AI-built apps.

FAQ

My table shows RLS enabled but data still leaks. Why?

The most likely reason is a USING (true) policy. RLS is on and a policy exists, but the policy's condition is always true, so it allows every row through to everyone — including anonymous visitors. The fix is to replace that condition with one that ties rows to their owner, like auth.uid() = user_id, then confirm an anonymous request returns no rows.

Why does my AI builder keep creating USING(true) policies?

Because it optimizes for making the feature work in the preview, where there's usually no logged-in user. A strict policy would return nothing in that anonymous preview and the feature would look broken, so the AI writes a permissive USING (true) policy to make the demo work. It technically enabled RLS, which looks careful, but it left the table fully readable.

Will a security scanner catch a USING(true) policy?

Only if it tests the data your endpoints actually return. A scanner that checks merely whether RLS is enabled will report "safe" and miss it, because USING (true) makes RLS appear on while protecting nothing. A scanner that queries your endpoints as a stranger and inspects the returned rows will catch it.

The bottom line

USING (true) is the most deceptive of Supabase RLS mistakes: the switch is on, a policy exists, and your entire table leaks anyway. The dashboard won't warn you, and a scanner that only checks whether RLS is enabled will tell you you're safe while a stranger reads your users' data. The only honest test is the rows that come back to an anonymous request. An attacker runs that test in minutes. The point is to run it yourself first — and to keep running it, because your AI can quietly add another USING (true) table on the next deploy.

Find your gaps before an attacker does.

Is My Site Hackable? scans your deployed app for the exact issues in this article — exposed keys, missing RLS, open buckets — and tells you what's real and what's a false alarm.

Run a free scan →