RLS Is On but Users Still See Each Other's Data
No — a green Lovable Security Scan does not mean your app is safe. The built-in scan only checks that Row-Level Security is enabled on each table, not that the policy is effective. A permissive policy like USING (true) passes the scan while leaving every row readable by any authenticated user. This is the 2026 nuance that catches teams who assumed 'RLS on' meant 'data protected.'
By Hire Lovable Xperts · Last verified: 2026-07-19
Does passing Lovable's Security Scan mean my app is safe?
No. Lovable's built-in Security Scan checks that Row-Level Security is enabled on your tables — not that the policies are effective. As the researcher who reported CVE-2025-48757 put it, the scan helps ensure RLS policies are enabled and notifies you if they are not, but 'doesn't necessarily indicate if they are sufficient.' A table can pass the scan and still be readable by every user.
The gap is between two very different questions. 'Is RLS turned on?' is a yes/no toggle the scan can read. 'Does the policy actually restrict rows to their owner?' requires reading the policy's condition — and a policy of USING (true) is enabled, so it passes, while granting unrestricted access. Enabled is not the same as effective.
What does a USING (true) policy actually do?
A Row-Level Security policy's USING clause is the condition PostgreSQL evaluates to decide which existing rows a request may see. USING (true) evaluates to true for every row, so it grants unrestricted read access to everyone the policy applies to. Supabase's own documentation shows exactly this pattern for deliberately public data — the danger is when it lands on a table of private, user-owned records.
Lovable sometimes generates USING (true) during prototyping: when enabling RLS makes data disappear from a view, the quickest way to make it reappear is a policy that allows everything. That satisfies the feature and the Security Scan in one move, and silently removes the access control. The CVE reporter explicitly warns against 'overly permissive policies, such as USING (true) for select operations on sensitive tables without additional restrictive conditions.'
USING vs WITH CHECK: which clause is leaking?
RLS policies have two conditions, and they guard different directions of data flow. USING controls which existing rows are visible or modifiable (SELECT, UPDATE, DELETE). WITH CHECK validates the contents of new or changed rows (INSERT, UPDATE). A permissive USING leaks reads; a missing or permissive WITH CHECK lets a user write rows they should not own. Most real exposure comes from a permissive USING on SELECT.
Per Supabase's documentation, if a policy omits WITH CHECK on an UPDATE, the USING expression is reused to validate the new row. So a single correctly scoped condition can cover both directions of an UPDATE — but only if that condition is auth.uid() = user_id, not true.
| Operation | Clause that applies | What a permissive value exposes |
|---|---|---|
| SELECT | USING | USING (true) returns every row to every user — the classic leak |
| INSERT | WITH CHECK | A permissive WITH CHECK lets users insert rows owned by someone else |
| UPDATE | USING + WITH CHECK | USING picks which rows are editable; WITH CHECK validates the result — both must be scoped |
| DELETE | USING | A permissive USING lets any user delete any row via the API |
Related: The four-policy CRUD template
How do I tell if my RLS policy is permissive?
Read the policy conditions directly in the Supabase SQL Editor — the dashboard's green checkmark will not tell you. One query against pg_policies returns the real USING and WITH CHECK expression for every policy in your schema. You are looking for auth.uid() somewhere in the condition; if you see true or an empty condition on a user-data table, that policy is permissive and the table is exposed.
- Open the Supabase SQL Editor from your project dashboard.
- Run: SELECT tablename, policyname, cmd, qual AS using_expression, with_check FROM pg_policies WHERE schemaname = 'public' ORDER BY tablename, cmd;
- For each user-data table, inspect the qual (USING) column: a correct policy shows something like (auth.uid() = user_id); a permissive one shows true or is blank.
- Check the with_check column the same way for INSERT and UPDATE rows — a blank or true value means writes are not owner-scoped.
- List every command: confirm SELECT, INSERT, UPDATE, and DELETE each have a scoped condition, not just SELECT.
How do I replace a permissive policy with an effective one?
Drop the permissive policy and recreate it scoped to the row owner. The safe pattern limits every operation to rows where user_id equals the authenticated user's ID, exposed by Supabase as auth.uid(). Run this in the SQL Editor, not the Lovable chat, so the AI cannot silently rewrite it back to USING (true) while fixing an unrelated feature.
Test the result with two real accounts. Sign in as each and confirm each sees only their own rows, then attempt a cross-account read of a known row ID via the API — a correctly scoped policy returns nothing. Passing the Security Scan again is not the test; two-account isolation is.
- Confirm the table has a user_id (uuid) column holding the owner; add and backfill one if missing before enabling RLS.
- Ensure RLS is on: ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;
- Drop the permissive policy: DROP POLICY IF EXISTS "<old policy name>" ON public.your_table;
- Recreate SELECT scoped: CREATE POLICY "Users read own rows" ON public.your_table FOR SELECT TO authenticated USING (auth.uid() = user_id);
- Add matching INSERT (WITH CHECK), UPDATE (USING + WITH CHECK), and DELETE (USING) policies, each using auth.uid() = user_id.
- Re-run the pg_policies query and confirm every condition shows auth.uid(), not true.
Why does the Security Scan give a false sense of safety?
Because it answers a narrower question than the one you care about. The scan verifies RLS is enabled — a fast, reliable check the platform can automate. Whether each policy is logically correct depends on its condition and on how PostgreSQL combines multiple policies, which the scan does not evaluate. A green result means 'RLS is on somewhere on this table,' not 'this table is protected.'
This is not unique to Lovable — any tool that checks for RLS enablement rather than policy effectiveness reports the same false positive. The reliable signal is the policy condition itself. Until you have read the qual and with_check for every command on every user-data table, and tested with two accounts, a passing scan is a starting point, not a guarantee.
Should I get an audit if my Security Scan is already green?
If your app handles payments, personal data, or messages between users, yes — a green scan is exactly the situation where a permissive policy hides in plain sight. An audit reads every policy condition, checks how policies combine, tests isolation with real accounts, and confirms writes are scoped, not just reads. It catches the effective-versus-enabled gap the built-in scan is structurally unable to see.
You can do this yourself for a small app: read the pg_policies output, confirm auth.uid() on every command, and verify with two accounts. Bring in an expert when there are many tables, when policies reference each other, or when sensitive data means a single missed permissive policy is a reportable exposure. Book a call and we will tell you which of your policies are effective and which only look it.
Related: Lovable Security Audit service · Book a free security call
Frequently asked questions
Does a passing Lovable Security Scan mean my app is safe?
What is the difference between RLS being enabled and effective?
Does USING (true) pass the Lovable Security Scan?
How do I check whether my RLS policy is permissive?
What is the difference between USING and WITH CHECK?
Can one bad policy override my correct policies?
My scan is green — should I still get an audit?
App down or leaking data? Get an expert on it within 24–48h.
Book a free 30-minute audit call. We'll diagnose what's wrong and tell you exactly what it costs to fix.