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.
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.
How to check
- Run the policy audit query below. Any policy on user data without an ownership comparison (= auth.uid()) needs a look.
- 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.
- Grep your edge functions for SUPABASE_SERVICE_ROLE_KEY and check each one gets the user from the Authorization header, not from the body.
How to fix
- Rewrite policies to compare the row’s owner column with auth.uid(), with a with check clause on insert and update (code below).
- 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.
- 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\(\)';