PL-007mediumLab

Public storage bucket

Files in this bucket are served to anyone with the link. ID scans and invoices don't belong there.

Severity
Medium
Who can exploit it
Anyone with, or able to list, the file links
Found by
Free RLS check (static)
Lab proof
Listing test included for your engineer

What it is

A public bucket serves its files to anyone who has the URL, with no sign-in. That is right for avatars and product photos. It is a leak when the bucket holds personal files: ID documents, CVs, invoices, signed contracts, medical letters.

It gets worse when a broad read policy on storage.objects also lets visitors list the bucket, because then nobody needs to guess the links.

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', true);

-- Often paired with a broad read policy that also allows listing
create policy "Anyone can view documents"
  on storage.objects for select
  using (bucket_id = 'documents');

Check it yourself

check-pl-007.sql
-- 1. Which buckets are public?
select id, name, public from storage.buckets where public;

-- 2. Who can read objects (and so list file names)?
select policyname, roles, cmd, qual
from pg_policies
where schemaname = 'storage' and tablename = 'objects';

The fix PatchLabs writes

PatchLabs makes the bucket private and lets each user read only the files in their own folder. The app then shows files through short-lived signed links.

supabase/migrations/…_patchlabs_fix.sql
update storage.buckets set public = false where id = 'documents';
drop policy "Anyone can view documents" on storage.objects;
create policy "Owners read own documents"
  on storage.objects for select
  to authenticated
  using (
    bucket_id = 'documents'
    and (storage.foldername(name))[1] = (select auth.uid())::text
  );

The code change that ships with it: replace getPublicUrl(path) with createSignedUrl(path, 60) wherever the app shows these files, and upload to <user id>/filename. The report lists each place.

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

listing test · storage.objects
[BEFORE]  anon list documents → 3 LEAK
[AFTER]   anon      → 0 objects     DENIED
          stranger  → 0 objects     DENIED
          owner     → 1, own folder OK
test SQL in the reportIllustrative output · fake files

The listing test reads storage.objects as each identity. It is included as SQL for your engineer; the automatic proof today covers your tables. Whether a public URL still works is decided by the bucket's public flag, which the fix sets to false.

When the pattern is intended

Public buckets are right for files meant for everyone: avatars, product images, logos, blog images. Keep them public, and keep personal files in a separate private bucket. If a public bucket only holds public assets, there is nothing to fix.

Supabase linter

0025 public_bucket_allows_listing
Flags public buckets whose object policies let clients list their files.

Guides

Free check

Does your app have PL-007?

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.

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.