LEAKCADE

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

  1. 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);
  2. For updates, add the same check to with check, so a user can't hand their row to someone else.
  3. If only you should write to a table, check an admin role from app_metadata or a roles table. Don't use user_metadata for that, because users can edit it themselves. Or switch off public signup if nobody else needs an account.
  4. Write one policy per operation. Supabase's docs recommend separate select, insert, update and delete policies and say a for all policy "hides which operation each rule was meant to cover."
  5. Test it. Supabase's docs point out that a delete blocked by a using clause raises no error and just matches zero rows. "No error" proves nothing, so test with returning and 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

  1. Symbiotic Security: We scanned 1,072 vibe coded apps
  2. Supabase docs: Row Level Security
  3. Supabase docs: General auth configuration
  4. Leakcade: Supabase RLS mistakes
  5. 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 repo

Leakcade 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

Patch Notes by Leakcade

A short weekly email: what changed in AI coding tools, one real security mistake, one prompt you can reuse.