Is your Supabase storage bucket public? How to check it and lock it down
By Pritesh Ghodge, security engineer · Last updated 1 November 2026
Want your live app checked from the outside?
Abbi Labs checks a deployed app for leaked secret keys, Supabase tables readable with the public key, exposed .env/.git files and security headers. The full scan of a domain you verify also flags public storage buckets. The first scan is free.
Weekly AI-app security checklist
One short email a week: a practical check for apps built with Supabase, Next.js and AI tools (RLS, leaked keys, headers and more). Unsubscribe anytime.
When an AI builder adds file uploads (profile photos, documents, receipts), the quickest way to make the images show up is a public bucket. That's fine for avatars. It's not fine when the same bucket ends up holding passports, invoices or private attachments, because every file in a public bucket can be downloaded by anyone who has its URL.
There are two separate questions, and you need to check both:
- Is the bucket public? Then anyone with a file's URL can download it, no login needed.
- Can strangers list the files? That's controlled by SELECT policies on
storage.objects. A broad one lets anyone enumerate the bucket, so they don't even need the URLs.
What Supabase says public and private mean
From the Supabase Storage bucket docs: buckets are private by default, and every operation on a private bucket, including downloads, goes through RLS policies on storage.objects. Private files are fetched with the user's session or with a signed URL that expires. A public bucket bypasses access control for retrieving and serving files, so anyone with the URL can read them. Uploading, deleting, moving and copying are still controlled by policies, even on a public bucket.
Check 1: which buckets are public
In the Supabase dashboard, open Storage: public buckets are labelled as public. Or run this in the SQL editor (read-only):
select id, name, public
from storage.buckets
order by name;For every row where public is true, ask: would I be fine with a stranger downloading any file in here? If the answer is no, that bucket needs to be private (see the fix below).
From the outside, you can also spot it in your live app: open DevTools → Network, load a page that shows an uploaded file, and look at the file URL. A path containing /storage/v1/object/public/<bucket>/ is served from a public bucket. A path with /object/sign/ and a token= parameter is a signed URL from a private bucket.
Check 2: who can list or change files
Storage permissions are RLS policies on the storage.objects table. List them:
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'storage' and tablename = 'objects'
order by cmd, policyname;What to look for:
- SELECT (or ALL) policies for
anon,authenticatedorpublicwhose condition is justtrueor justbucket_id = '…'. These let anyone (or any signed-in user) list and read every file in that bucket. On a public bucket you usually don't need a SELECT policy at all: Supabase notes public buckets are already publicly accessible by URL. Supabase's Security Advisor has a warning for exactly this case, "Public bucket allows listing". - INSERT with
with check (true), or with no role restriction. Anyone with your public key can upload files: your storage bill, your domain hosting their content. Restrict uploads toauthenticatedand to the user's own folder. - UPDATE or DELETE policies that don't mention the user (no
auth.uid(),auth.jwt()orowner_id). Any signed-in user can then overwrite or delete other people's files.
The table RLS guide covers the same using (true) trap for your normal tables.
Check 3: can a stranger list your bucket?
Only test your own project. With the public (anon / publishable) key from your app, ask Storage to list a bucket:
curl -s -X POST "https://YOUR-PROJECT.supabase.co/storage/v1/object/list/BUCKET" \
-H "apikey: YOUR_PUBLIC_KEY" \
-H "Authorization: Bearer YOUR_PUBLIC_KEY" \
-H "Content-Type: application/json" \
-d '{"prefix":"","limit":5}'An empty list [] means a logged-out visitor can't list the top level of that bucket (or it's empty). If you get file or folder names back, anyone can enumerate it. Repeat with a folder name as the prefix if your files live in per-user folders.
Fix: make private files private
1. Switch the bucket to private
In Storage, edit the bucket and turn off Public bucket, or in SQL:
update storage.buckets set public = false where id = 'documents';Public URLs for that bucket stop working, so do step 3 at the same time or images and downloads in your app will break.
2. Give each user their own folder, enforced by policy
Upload files to <user id>/<file name>, then only let users touch their own folder. This follows the pattern in Supabase's storage access control docs:
-- read own files
create policy "read own files" on storage.objects
for select to authenticated
using (bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'));
-- upload only into own folder
create policy "upload own files" on storage.objects
for insert to authenticated
with check (bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'));
-- replace / delete only own files
create policy "update own files" on storage.objects
for update to authenticated
using (bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'))
with check (bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'));
create policy "delete own files" on storage.objects
for delete to authenticated
using (bucket_id = 'documents'
and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'));Then drop any older broad policies on that bucket that you found in check 2. Policies are OR'd together, so one leftover using (true) undoes all of the above.
3. Show files with signed URLs
Instead of a permanent public URL, ask for a link that expires, from the signed-in user's session:
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl(`${user.id}/receipt.pdf`, 60); // valid for 60 seconds4. If the bucket really should be public, stop the listing
For avatars and public media, keep the bucket public but remove broad SELECT policies on it (from check 2). Files still load by URL; strangers just can't enumerate them. Keep INSERT, UPDATE and DELETE limited to each user's own folder.
5. If private files were public
Treat them as possibly copied. Making the bucket private stops new downloads by URL, but it can't recall copies someone already took. Work out which files were in the bucket and for how long, and follow your privacy obligations if personal information was involved.
A prompt for Lovable, Bolt or Cursor
Audit Supabase Storage in this project:
1. List every bucket and whether it is public. For any bucket holding
user uploads that aren't meant to be public, make it private.
2. Store each user's files under a folder named with their user id.
3. Replace storage.objects policies so users can only select, insert,
update and delete files in their own folder (check the first folder
against auth.jwt()->>'sub'). Remove any policy that uses true or only
checks bucket_id for anon, authenticated or public.
4. Show private files with createSignedUrl instead of public URLs.
5. Show me the SQL and the list of policies you are dropping before
you apply anything.Then run checks 1 to 3 again. AI fixes sometimes make an upload error go away by adding with check (true).
What an outside scan can and can't tell you
From outside, a scanner can see which buckets your public key can list and which are marked public. It can't see what's inside your files, whether your per-folder policies are right for signed-in users, or a policy that only applies after login. For those, use the SQL above and test with two accounts: upload a file as user A, then try to open or list it as user B.
Keep it that way
New upload features tend to arrive with a new bucket. Abbi Labs scans your live app for leaked keys, Supabase tables and public buckets readable with your public key, exposed .env/.git files and security headers. Monitor re-scans weekly and after every deploy, and emails you only when something new appears.
FAQ
What does a public bucket mean in Supabase?
Supabase's docs say a public bucket bypasses access control for retrieving and serving files: anyone who has a file's URL can download it, logged in or not. Uploads, deletes, moves and copies are still controlled by RLS policies on storage.objects. Buckets are private by default.
Is it OK to have a public bucket?
Yes, for files that are meant to be public: avatars, product images, blog media. It's a problem when the bucket holds anything a user would expect to be private, such as uploaded documents, invoices, ID photos, exports or chat attachments.
If nobody knows the file URL, is a public bucket safe enough?
Not for private files. URLs end up in page source, emails, logs and browser history, and file paths are often guessable (a user id plus a file name). If storage policies also let anyone list the bucket, they don't need the URLs at all.
How do I let users see their own private files?
Make the bucket private, store each user's files under a folder named with their user id, add storage.objects policies that compare the first folder to the signed-in user, and show files with createSignedUrl (a link that expires) or download with the user's session.
Does Abbi Labs check storage buckets?
The full scan (for domains you've verified you own) uses the public key from your app to try listing your Supabase storage buckets and flags any bucket marked public, with the fix. The no-sign-up quick check on the home page skips storage. Neither looks inside your files, and the free RLS policy checker currently skips storage.objects, so check storage policies with the SQL on this page.
Related guides
- How to check if your Supabase tables are readable by anyone
- Free tool: Supabase RLS policy checker
- Lovable app security checklist (from a security engineer)
- Supabase "42501 permission denied for table": fix it with a GRANT, not by turning off RLS
- Supabase service_role key exposed? Do this in the next 15 minutes
- OpenAI, Claude or Gemini API key exposed in your frontend? What to do now