Skip to content

Public Supabase Storage buckets: are your users' files exposed?

Avatars can be public. ID scans, invoices and contracts cannot. How to check your Supabase Storage buckets and rules, and fix them without breaking uploads.

4 min read

Most apps built with Lovable, Bolt and similar tools let users upload something: a profile photo, a receipt, a contract, a passport scan for verification. On Supabase, those files go to Storage, organized in buckets.

When an AI tool creates a bucket, it often makes it public, because that makes images show up without extra work. That is fine for a logo. For anything personal, it means anyone with the link can download the file, and sometimes anyone at all can list every file in the bucket.

This guide is the Storage half of our 10-minute Supabase check. It takes about as long.

Public and private buckets

Public bucket Private bucket (the default)
Downloading a file Anyone with the URL, no rules checked Only if a rule allows it, or with a signed link that expires
Uploading, deleting, moving Controlled by your rules Controlled by your rules
Listing the files Controlled by your rules Controlled by your rules

Two things follow from this table:

  1. A public bucket skips your rules for downloads entirely. If file paths are predictable (invoices/1043.pdf, ids/user-15.jpg), a stranger can guess them.
  2. Making a bucket private doesn’t help if the rules on storage.objects let anyone read everything. Storage rules are ordinary Row Level Security policies on that table, and a generous one opens a private bucket just as wide.

Step 1: list your buckets

In the Supabase dashboard, open the SQL Editor and run:

Buckets, visibility and upload limits
select id, public, file_size_limit, allowed_mime_types
from storage.buckets
order by public desc, id;

For every row with public = true, ask one question: would I be comfortable if every file in this bucket appeared on a search engine? Product images, yes. Anything a user uploaded about themselves, no.

Also look at the last two columns. Empty limits mean anyone allowed to upload can store a file of any size and any type in your project, at your cost.

Step 2: read the Storage rules

Every rule on storage.objects
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'storage' and tablename = 'objects'
order by cmd, policyname;

What to look for:

  • A SELECT rule for anon or public whose condition is only bucket_id = '…'. That lets anyone list and read every file in the bucket.
  • An INSERT rule for anon, or for authenticated without a folder condition. That lets any user upload anywhere, including over other users’ paths if an UPDATE rule is also open.
  • A DELETE rule that doesn’t tie the file to its owner.

The dashboard’s Security Advisor flags some of these too. Check it after any change.

Step 3: confirm from the outside

Using only your project URL and public key (under Project Settings → API Keys), try to list a bucket the way a stranger would:

List files in a bucket with the public key
curl -X POST "https://YOUR-PROJECT.supabase.co/storage/v1/object/list/BUCKET_NAME" \
  -H "apikey: YOUR_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prefix": "", "limit": 20}'

If this returns file names from a bucket that holds private files, the listing is open. Repeat for each bucket that holds anything personal.

How to fix it

Move personal files to a private bucket

Create a private bucket (for example documents) for anything that identifies a person or contains business data. Keep a public bucket only for files that are meant to be public.

Then change how the app shows those files. Instead of a permanent public URL, it asks Supabase for a signed URL that expires, for example after an hour. With the JavaScript client, that is createSignedUrl(path, 3600). Old public links stop working when you switch, so plan this change together with the code change, not before it.

Give each user their own folder

The common pattern is to upload each file under a folder named after the user’s ID, such as documents/8f1c…/contract.pdf, and write rules that check it:

Users can read and upload only in their own folder
create policy "Read own documents"
on storage.objects for select
to authenticated
using (
  bucket_id = 'documents'
  and (storage.foldername(name))[1] = (select auth.uid()::text)
);

create policy "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)
);

Add update and delete rules with the same condition if users can replace or remove their files. Uploading with “overwrite” turned on needs select, insert and update.

Keep public buckets readable but not listable

A public bucket for avatars doesn’t need a SELECT rule at all for downloads, since public URLs bypass the rules. Remove broad SELECT rules for anon on public buckets unless the app really needs to list their contents.

Set limits

Give every bucket a maximum file size and a list of allowed types (for example image/png, image/jpeg, application/pdf). It takes a minute in the bucket settings and stops your Storage from becoming free hosting for someone else.

If your app handles documents, IDs or payment records and you would like someone to look at it with you, our free Security Check reviews Storage along with database rules, keys and authentication, read-only and from the outside.

SupabaseRow Level SecurityAI-built apps

This article is also available in: PortuguêsEspañol日本語中文

All articles