← All advisories
VV-047criticalBroken access control2026-10-10

Supabase RLS policies that check login but not ownership

A policy like using (auth.uid() is not null) has RLS on and mentions the user, so it passes Lovable’s scan. But it lets any logged-in user read every row, and on most apps anyone can sign up.

01.

What it is

A Lovable-focused audit firm (dev.to, October 2026) says the most common leak it now finds is not RLS switched off but an app with a green “passed” badge and a profiles policy using (auth.uid() is not null). “It says any logged-in user can read every row,” and the line should be auth.uid() = id. Automated checks “are good at presence” and “weak at intent”.

The same write-up says this often appears after someone hits “new row violates row-level security policy” and asks the AI to fix it, and the fix loosens the policy. It shows up on profiles, messages, orders and documents tables.

A related hole: edge functions that use the service_role key (which bypasses RLS) and trust a user_id sent in the request body. Those must read the user from the JWT and ignore what the client claims. verify_jwt = false in supabase/config.toml is sometimes needed (a Stripe webhook) but is often flipped just to clear a CORS error.

An r/vibecoding audit of 5 public AI-generated repos found all 5 leaking data, mainly via policies like using (true) on users tables. In r/SaaS, an auditor reported 17 of 30 AI-built MVPs had RLS missing or loose enough that any logged-in user could read other users’ data.

02.

How to check

  1. Run the policy audit query below. Any policy on user data without an ownership comparison (= auth.uid()) needs a look.
  2. Two-account test: sign up as A and B, copy a Supabase request from B’s session in dev tools, remove the id filter and resend. More than one row back means a leak, whatever the scan says.
  3. Grep your edge functions for SUPABASE_SERVICE_ROLE_KEY and check each one gets the user from the Authorization header, not from the body.
03.

How to fix

  1. Rewrite policies to compare the row’s owner column with auth.uid(), with a with check clause on insert and update (code below).
  2. In service_role edge functions, call supabase.auth.getUser() with the request’s Authorization header and use that user’s id. Never trust user_id or plan from the client.
  3. When the AI “fixes” an RLS error, read the new policy before accepting it.
-- WRONG: any logged-in user reads every row (passes "RLS enabled" scans)
-- create policy "Users can view documents" on public.documents
--   for select using (auth.uid() is not null);

-- RIGHT: users see and change only their own rows
alter table public.documents enable row level security;
drop policy if exists "Users can view documents" on public.documents;

create policy "Users read own documents" on public.documents
  for select to authenticated using ((select auth.uid()) = user_id);
create policy "Users insert own documents" on public.documents
  for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "Users update own documents" on public.documents
  for update to authenticated
  using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);

-- Audit: policies with no "= auth.uid()" ownership comparison
select tablename, policyname, cmd, qual, with_check
from pg_policies
where schemaname = 'public'
  and coalesce(qual, with_check, '') !~ 'auth\.uid\(\).*=|=.*auth\.uid\(\)';
04.

Sources

Related on VibeVulns
More advisories for this tool
The weekly email

One email a week. Only what changed.

New advisories, CVEs and fixes for Lovable, Bolt, v0, Replit, Base44 and Cursor apps. No roundups of things you already read.

Free. The list is new, so the first issues go out once there are readers. Unsubscribe from any email in one click. We never sell or share addresses.