Any signed-in user can delete your Supabase data
The policy using (auth.role() = 'authenticated') reads like "only my users". In practice it lets any account delete or edit any row, and if your project allows signups, anyone can get an account.
Why this matters
Symbiotic Security scanned 1,072 Supabase apps built with Lovable, v0, Bolt, Replit and Windsurf and published the results in June 2026. They report 172 sites that let anyone delete records without logging in, and 172 that let anyone change existing records. Those numbers are for visitors who never signed in. A policy that only checks for a login lets in everyone who spends ten seconds creating an account.
No DELETE policy at all is the safe case. With RLS on, a delete that no policy allows matches zero rows and changes nothing.
Three patterns to look for
Logged in means allowed. The policy checks that there is a user, not that the row is theirs:
create policy "Authenticated users can delete" on public.posts for delete using (auth.role() = 'authenticated'); create policy "Users can delete their own codes" on public.codes for delete using (auth.uid() is not null);
The second is from a public repo: its name says "their own", but the expression only checks for a login. Storage has the same shape: bucket_id = 'avatars' and auth.role() = 'authenticated' lets any user delete anyone's files in that bucket.
Deletes for everyone. Without a to role, a policy applies to public, which includes signed-out visitors: for delete using (is_custom = true) lets anyone with your anon key delete every custom row.
RLS switched off later. An early migration enables RLS, and a later "quick fix" runs alter table ... disable row level security to get rid of a permission error. Every policy on that table stops counting.
"My app has no signup page"
This is a common reason these policies feel safe, especially on portfolio and CMS projects where the owner is the only one who logs in. The Supabase signup endpoint doesn't need your page. Unless you switched off "Allow new users to sign up" in the Auth settings, anyone can call supabase.auth.signUp({ email, password }), or the Auth API directly, with the anon key your app already ships.
What we found in public repos
We searched public GitHub code for delete policies that use auth.role() = 'authenticated' and read the SQL files from 28 repos. Our check flagged 18 of them, with 78 policies between them that let any signed-in user delete or change rows that aren't theirs. One of them was open to visitors who never signed in. A typical case was a portfolio's admin tables (profiles, projects, site settings, contact messages) with for all using (auth.role() = 'authenticated'). In a second sample of 15 repos with auth.uid() is not null, 9 were flagged. In a control sample of 15 repos whose delete policies compare auth.uid() with the row's owner, it flagged none.
We picked these files because they matched the pattern, so this is not a rate across all Supabase apps. We read every flagged policy by hand.
How to fix it
- Compare the user with the row's owner, and name the role:
create policy "own rows: delete" on public.posts for delete to authenticated using ((select auth.uid()) = user_id);
- For updates, add the same check to
with check, so a user can't hand their row to someone else. - If only you should write to a table, check an admin role from
app_metadataor a roles table. Don't useuser_metadatafor that, because users can edit it themselves. Or switch off public signup if nobody else needs an account. - Write one policy per operation. Supabase's docs recommend separate select, insert, update and delete policies and say a
for allpolicy "hides which operation each rule was meant to cover." - Test it. Supabase's docs point out that a delete blocked by a
usingclause raises no error and just matches zero rows. "No error" proves nothing, so test withreturningand check that the row is still there.
Fix prompt you can paste
Review every RLS policy in supabase/migrations for DELETE, UPDATE and ALL. Flag any policy whose USING clause only checks auth.role() = 'authenticated' or auth.uid() IS NOT NULL, any write policy without a TO role that never checks auth.uid(), storage.objects policies that only check bucket_id and login, and any later DISABLE ROW LEVEL SECURITY. Rewrite each one with TO authenticated and an ownership check ((select auth.uid()) = user_id), add WITH CHECK to updates, use app_metadata or a roles table for admin-only tables, and write pgTAP tests where a second user tries to delete the first user's row and the row is still there afterwards.
How Leakcade checks this
Our policy check reads your SQL migrations in order. It flags DELETE, UPDATE and ALL policies that only check for a login, write policies open to signed-out visitors without any auth check, and tables where RLS is switched off again in a later migration. Policies with using (true) and tables created without RLS are covered by our auth-route check. Changes made only in the Supabase dashboard don't show up in migrations, and the report tells you to compare.
On the 15 repos that scoped deletes to the row's owner, the check stayed quiet.
Further reading
Sources
- Symbiotic Security: We scanned 1,072 vibe coded apps
- Supabase docs: Row Level Security
- Supabase docs: General auth configuration
- Leakcade: Supabase RLS mistakes
- Leakcade: Supabase user_metadata roles
Want a second pair of eyes?
We review your whole repo and write a fix prompt for every finding. $19, report within an hour.
Check my repoLeakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms