PL-015highcoming · not tested yet

Stored files readable or writable by anyone

The bucket is private, but its storage policies let anyone read, upload, replace or delete other people's files.

Severity
High
Who can exploit it
Anyone the policy applies to, often anon
Found by
Not tested yet: coming to PatchLabs Lab
Status
Coming

PatchLabs does not test for this yet (coming). This page explains the problem, how to check your own project and how to fix it. The Lab’s live checks today are PL-001 to PL-008; this one is not part of any report until it ships. Shipping dates go in the changelog.

What it is

Supabase Storage keeps a row for every file in storage.objects, and row level security policies on that table decide who can download, list, upload, replace and delete files through the Storage API. PL-015 means one of those policies checks only the bucket, for example using (bucket_id = 'documents'), so it covers every file in the bucket and, with no to clause, logged-out visitors as well.

This is different from PL-007. A public bucket serves files by URL to anyone, without checking the read policy. A private bucket with a broad read policy is just as open to anyone who asks the API, and a broad insert, update or delete policy is worse: anyone can upload into your bucket, replace another user's file, or delete it.

Why AI builders produce it

Uploading is where builders meet their first storage error, "new row violates row-level security policy". The quick fix is one policy per command that names only the bucket. The safer per-user folder pattern needs the upload path to start with the user's id, which means changing the upload code too, so it gets skipped.

Example of vulnerable code

Simplified from the patterns we see in AI-built apps. Synthetic: not taken from any real app.

supabase/migrations/…_storage.sql (vulnerable)
insert into storage.buckets (id, name, public)
values ('documents', 'documents', false);  -- private, so it looks safe

create policy "Allow uploads" on storage.objects
  for insert with check (bucket_id = 'documents');
create policy "Allow reads" on storage.objects
  for select using (bucket_id = 'documents');
create policy "Allow deletes" on storage.objects
  for delete using (bucket_id = 'documents');

Check it yourself

check-pl-015.sql
-- 1. Every policy on storage.objects, ones that never mention the user first
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'storage' and tablename = 'objects'
order by (coalesce(qual, '') || coalesce(with_check, '')) ilike '%auth.uid()%', cmd;

-- 2. File names a logged-out visitor can see (nothing is changed)
begin;
set local role anon;
select name from storage.objects where bucket_id = 'documents' limit 20;
rollback;

The fix

Give each user a folder named after their id, and let them touch only that folder.

supabase/migrations/…_fix_storage.sql
drop policy "Allow uploads" on storage.objects;
drop policy "Allow reads" on storage.objects;
drop policy "Allow deletes" on storage.objects;
create policy "Users upload to own folder" on storage.objects
  for insert to authenticated
  with check (bucket_id = 'documents'
    and (storage.foldername(name))[1] = (select auth.uid())::text);
create policy "Users read own files" on storage.objects
  for select to authenticated
  using (bucket_id = 'documents'
    and (storage.foldername(name))[1] = (select auth.uid())::text);
create policy "Users delete own files" on storage.objects
  for delete to authenticated
  using (bucket_id = 'documents'
    and (storage.foldername(name))[1] = (select auth.uid())::text);

The code change that goes with it: upload to `${user.id}/${file.name}`, and show files with createSignedUrl(path, 60). Replacing a file with upsert: true also needs matching select and update policies.

PatchLabs doesn’t write this fix yet. Review the SQL with your engineer and add it as a new migration.

How PatchLabs will test it

When this check ships, the Lab will run it the way it runs PL-001 to PL-008 today: rebuild your database from supabase/migrations in a disposable Postgres, seed fake rows for two users, and try the exploit as an anonymous visitor, a signed-in stranger and the owner. Until then, the self-check above is the test.

When the pattern is intended

Public reads of public assets, such as avatars or product photos, are fine: use a public bucket or a read policy limited to that bucket. Anonymous uploads are rarely intended. If a public form must accept files, limit the bucket's file size and allowed file types in its settings, and never let the public read that bucket back.

Why it’s rated high

High. Reading other people's files often means ID documents, invoices or contracts, and write access lets anyone replace a file your users will download and trust.

Guides

Free check

Check what PatchLabs does test today

This check is coming. Today’s free check rebuilds your database from your migrations, tests it as a visitor, a stranger and the owner (PL-001 to PL-008), and reads your repo for keys and risky code.

Free for your first appReads your public repo onlyOr run the free static RLS check

Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.