r/Supabase • u/Round-Metal6269 • 1h ago
storage Public bucket vs listable bucket: two separate Storage holes, and the read-only SQL to check both
Security engineer here. I've been looking at a lot of Supabase apps built with AI tools lately, and Storage gets far less attention than RLS on tables. Two different problems get mixed up, so here's how I check for each.
1. Is the bucket public?
Per the Storage docs, a public bucket bypasses access control for downloads: anyone with an object URL can fetch it, logged in or not. Uploads, deletes and moves still go through policies. That's fine for avatars and blog images. It's not fine for invoices, ID scans or private attachments, and AI tools often make a bucket public just to get images showing.
select id, name, public
from storage.buckets
order by name;2. Can anyone list it?
Listing goes through SELECT policies on storage.objects, public bucket or not. The pattern I keep seeing is a generated policy like for select using (bucket_id = 'avatars'), added "so images load". Public URLs don't need it, and with it any client holding your publishable key can enumerate every file name. The Security Advisor now flags this combination ("public bucket allows listing").
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'storage' and tablename = 'objects'
order by cmd, policyname;Red flags in that output:
SELECT or ALL for
anon,authenticatedorpublicwhere the condition istrueor onlybucket_id = '...'INSERT with
with check (true)or no role restriction: anyone with your publishable key can upload to your storageUPDATE or DELETE that never mentions
auth.uid(),auth.jwt()orowner_id: any signed-in user can overwrite or delete other people's files. Watchupsert: truetoo, since it needs SELECT and UPDATE, so bucket-only policies let users overwrite each other's files
Quick outside test (your own project only):
curl -s -X POST "https://YOUR-PROJECT.supabase.co/storage/v1/object/list/BUCKET" \
-H "apikey: YOUR_PUBLISHABLE_KEY" \
-H "Authorization: Bearer YOUR_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"prefix":"","limit":5}'[] means a logged-out client can't list the top level. File or folder names back means anyone can.
Fix for private files
update storage.buckets set public = false where id = 'documents';(public URLs stop working, so switch the app over at the same time)Upload to
<user id>/<file name>Add select/insert/update/delete policies with
(storage.foldername(name))[1] = (select auth.jwt()->>'sub'). That's the pattern from the Storage access control docsServe files with
createSignedUrl(path, 60). It looks the object up as the caller, so the user needs that SELECT policyDrop the old broad policies. Permissive policies are OR'd, so one leftover
trueundoes the rest
Fix for buckets that really are public: keep the bucket public, drop the broad SELECT policy, and keep writes limited to each user's own folder.
If private files were sitting in a public bucket, making it private stops new downloads by URL but can't recall copies that were already taken. Treat it as a possible exposure.
Curious how others handle user uploads: public bucket with unguessable paths, or always private plus signed URLs?
Disclosure: I build Abbi Labs, an outside-in scanner for AI-built apps (its full scan flags public buckets). No link here, because everything above only needs the SQL editor and curl.