Anonymous visitor can read personal data
Anyone with your app's public key can read emails, phone numbers or addresses.
- Severity
- Critical
- Who can exploit it
- Anyone on the internet
- Found by
- Free RLS check (static) and the full lab check
- Lab proof
- Automatic: anon, stranger and owner reads
What it is
Every Supabase app ships a public key (the anon key) inside its web pages. That is by design: the key is meant to be public, and row level security (RLS) policies decide what it can read. PL-001 means a table holding personal data, such as emails, phone numbers, addresses or dates of birth, has a read policy that lets the anon role through without checking who is asking.
Nobody needs to break in. Anyone who opens your site can copy the key from the page and ask the database for every row. The usual cause is a policy written as using (true) with no to clause, which applies to every role, including logged-out visitors.
In our lab study, 70 of 318 rebuilt apps (22%) let anyone with the public key read personal data, through this pattern or PL-008 (PatchLabs lab study, n=318, Nov 2026).
Example of vulnerable code
Simplified from the patterns we see in AI-built apps. Synthetic: not taken from any real app.
create table public.profiles (
id uuid primary key references auth.users (id) on delete cascade,
full_name text,
email text,
phone text
);
alter table public.profiles enable row level security;
-- No "to" clause: this applies to every role, including anon.
create policy "Profiles are viewable by everyone"
on public.profiles for select
using (true);
Check it yourself
Run these in the Supabase SQL editor of your own project. The second query reads the table exactly as a logged-out visitor would, inside a transaction that is rolled back, so nothing changes.
-- 1. Read policies that apply to logged-out visitors and don't check the user
select tablename, policyname, roles, qual
from pg_policies
where schemaname = 'public'
and cmd in ('SELECT', 'ALL')
and roles && array['anon', 'public']::name[]
and coalesce(qual, '') not ilike '%auth.uid()%';
-- 2. Read a table as an anonymous visitor (nothing is changed)
begin;
set local role anon;
select count(*) from public.profiles; -- more than 0 means anyone can read it
rollback;
Tables with RLS switched off never appear in pg_policies. Check those with PL-008.
The fix PatchLabs writes
When the rows belong to individual users, PatchLabs replaces the open policy with an owner-only one. Using (select auth.uid()) rather than auth.uid() lets Postgres evaluate it once per query instead of once per row.
-- supabase/migrations/<timestamp>_patchlabs_leak_fix.sql
drop policy "Profiles are viewable by everyone" on public.profiles;
create policy "Users can view own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = id);
When a public page genuinely needs some columns (a profile page showing a name and avatar), the fix keeps the rows readable and withholds the personal columns from logged-out visitors:
-- Visitors keep the public columns; contact details are withheld.
revoke select on public.profiles from anon;
grant select (id, full_name) on public.profiles to anon;
-- The app must now ask for those columns by name: select('id, full_name').
-- select('*') will fail for logged-out visitors, so the report lists that code change.
Signed-in users are a separate question. If anyone can sign up, see PL-002.
Every fix arrives as a migration file (plus any code change it needs, noted in the report) for your engineer to approve. Download it, or commit it on GitHub in one click from the report page. Automatic pull requests need our GitHub App, which is coming.
The proof it runs
[BEFORE] anon → 2 rows LEAK email •••••@••••• phone +•• •••• [FIX] owner-only select policy [PROVE] anon → 0 rows DENIED stranger → 0 rows DENIED owner → 1 row, own OK [DONE] lab proof passed
This is the check every PatchLabs fix must pass. The lab rebuilds your database from supabase/migrations, seeds fake rows for two users, then reads each flagged table as the anonymous visitor, a signed-in stranger and the owner. A fix only counts as proven when the visitor and the stranger read neither the private rows nor the personal columns, and the owner still reads their own row, personal columns included.
When the pattern is intended
Some public data is meant to be public. A shop's own phone number, a venue's address or a business's contact email on its listing page are not anyone's private data. In our fix study, 4 of 24 flagged apps (about 1 in 6) turned out to be exactly this, and the agents spotted it themselves (n=24, 5 Nov 2026).
When the fix agent judges a table to be public by design, the report says so, explains why, and asks you to confirm in plain English with a recommended answer. If you confirm, the table is remembered as intended and won't alert again.
Supabase linter
- 0024 permissive_rls_policy
- The closest lint, but it does not fire here: by design it skips
SELECTpolicies, because public reads are often intended. - 0023 sensitive_columns_exposed
- Only fires on tables with RLS switched off, and its column list does not include email, phone or address.
Based on the linter's source code and its maintainers' review notes, checked Nov 2026. This is why behaviour testing matters: the lint is right that a public read can be intended, so only knowing what the table holds and who should see it settles the question.
Related findings
Any signed-in user can read every row
Signing up unlocks other people's rows. If sign-up is open, that means anyone.
PL-005Always-true policy
A policy that says yes to everyone. On writes, anyone can change or delete your data.
PL-008Row level security not enabled
No rules at all. With API access granted, anyone with the public key can read and change every row.
Guides
- What to do in the first hour after a leak
- Supabase RLS policies explained, with owner and stranger tests
- How to secure a Lovable app’s Supabase database
- Supabase Realtime security: keep private rows out of subscriptions
- Supabase views and security_invoker: stop views bypassing RLS
- Pre-launch security checklist for vibe-coded apps
Free check
Does your app have PL-001?
Paste your public repo. We rebuild the database from your migrations, test it as a visitor, a stranger and the owner, and hand you the proven fix.
Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.