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.
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
-- 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.
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.
Related findings
Public storage bucket
Files in this bucket are served to anyone with the link. ID scans and invoices don't belong there.
PL-001Anonymous visitor can read personal data
Anyone with your app's public key can read emails, phone numbers or addresses.
PL-012 · comingStrangers can change or delete other people's rows
A write policy checks that someone is signed in, not that the row is theirs. Anyone who signs up can edit or wipe it.
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.
Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.