Hire Lovable Xperts
Security

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.

Related: The Lovable breach and CVE-2025-48757, explained

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 (true) on a user-data table is functionally identical to having RLS switched off — every authenticated request reads every row. It passes the Security Scan because RLS is technically enabled. Never leave it on a table that holds data scoped to a user.

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.

Which RLS clause governs each operation
OperationClause that appliesWhat a permissive value exposes
SELECTUSINGUSING (true) returns every row to every user — the classic leak
INSERTWITH CHECKA permissive WITH CHECK lets users insert rows owned by someone else
UPDATEUSING + WITH CHECKUSING picks which rows are editable; WITH CHECK validates the result — both must be scoped
DELETEUSINGA 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.

  1. Open the Supabase SQL Editor from your project dashboard.
  2. Run: SELECT tablename, policyname, cmd, qual AS using_expression, with_check FROM pg_policies WHERE schemaname = 'public' ORDER BY tablename, cmd;
  3. 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.
  4. Check the with_check column the same way for INSERT and UPDATE rows — a blank or true value means writes are not owner-scoped.
  5. List every command: confirm SELECT, INSERT, UPDATE, and DELETE each have a scoped condition, not just SELECT.
Permissive is also PostgreSQL's default combination mode: multiple policies on one table are OR-ed together, so a single USING (true) policy makes the table public even if a correctly scoped policy also exists. One permissive policy overrides all the careful ones.

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.

  1. Confirm the table has a user_id (uuid) column holding the owner; add and backfill one if missing before enabling RLS.
  2. Ensure RLS is on: ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;
  3. Drop the permissive policy: DROP POLICY IF EXISTS "<old policy name>" ON public.your_table;
  4. Recreate SELECT scoped: CREATE POLICY "Users read own rows" ON public.your_table FOR SELECT TO authenticated USING (auth.uid() = user_id);
  5. Add matching INSERT (WITH CHECK), UPDATE (USING + WITH CHECK), and DELETE (USING) policies, each using auth.uid() = user_id.
  6. Re-run the pg_policies query and confirm every condition shows auth.uid(), not true.

Related: Full two-account verification walkthrough

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?
No. Lovable's Security Scan checks that Row-Level Security is enabled on each table, not that the policies are effective. The researcher who reported CVE-2025-48757 notes the scan 'doesn't necessarily indicate if [policies] are sufficient.' A table with a permissive policy like USING (true) passes the scan while remaining readable by every authenticated user. Read the policy conditions directly to know if you are actually protected.
What is the difference between RLS being enabled and effective?
Enabled means RLS is switched on for the table, so PostgreSQL consults policies before returning rows. Effective means those policies actually restrict rows to their owner. A table can be enabled but ineffective: a USING (true) policy is enabled and passes automated scans, yet grants unrestricted access. The only proof of effectiveness is a policy condition comparing auth.uid() to the row's owner column, verified with two accounts.
Does USING (true) pass the Lovable Security Scan?
It can. The scan checks whether RLS is enabled, and a USING (true) policy requires RLS to be enabled in order to exist — so the table shows as protected. But USING (true) evaluates to true for every row, granting all users unrestricted read access. This is the exact scenario the CVE reporter warns about: an overly permissive policy on a sensitive table with no additional restrictive condition.
How do I check whether my RLS policy is permissive?
Run SELECT tablename, policyname, cmd, qual, with_check FROM pg_policies WHERE schemaname = 'public' in the Supabase SQL Editor. Read the qual (USING) column for each user-data table. A scoped policy shows a condition like (auth.uid() = user_id); a permissive one shows true or is blank. Check with_check the same way for INSERT and UPDATE. Any true or blank condition on user data means the table is exposed.
What is the difference between USING and WITH CHECK?
USING controls which existing rows a request can see or modify — it applies to SELECT, UPDATE, and DELETE. WITH CHECK validates the contents of new or changed rows — it applies to INSERT and UPDATE. Per Supabase's docs, if an UPDATE policy omits WITH CHECK, the USING expression is reused to validate the new row. A permissive USING leaks reads; a permissive WITH CHECK lets users write rows they should not own.
Can one bad policy override my correct policies?
Yes. PostgreSQL combines multiple permissive policies on a table with OR logic, so if any policy allows an action it proceeds. A single leftover USING (true) policy therefore makes the whole table public even if a correctly scoped policy also exists. To find these, list every policy per table with pg_policies and remove any permissive one, rather than assuming your good policy wins.
My scan is green — should I still get an audit?
If your app handles sensitive data, yes — a green scan is precisely where permissive policies hide. An audit reads every policy's condition, checks how policies combine, verifies both reads and writes are owner-scoped, and tests isolation with real accounts. That closes the enabled-versus-effective gap the built-in scan cannot evaluate. For a small app with a couple of clearly owned tables, you can run the same checks yourself.

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.

Get emergency help