LR-10

Customer Files Were Public All Along.

Your app lets users upload files — profile photos, invoices, contracts, documents, ID scans. The files go into Supabase Storage. The app shows each user their own files. Everything works.

But Supabase Storage has its own access control layer, separate from your database tables. A storage bucket marked as "public" serves files to anyone with the URL — regardless of RLS policies, regardless of authentication, regardless of who uploaded the file.

If the file URLs follow a predictable pattern — invoice_001.pdf, invoice_002.pdf, user_123/profile.jpg — anyone who guesses or enumerates the pattern can download every file in the bucket. No login required. No token needed. Just a URL.

This is a separate problem from database exposure. Your tables can have perfect RLS, your auth can be correctly enforced, and your storage bucket can still be wide open — because storage access control is configured independently.


Who This Is For

  • Founders whose app stores user-uploaded files in Supabase Storage
  • Developers who aren't sure whether their storage buckets are public or private
  • Teams that store sensitive documents — invoices, contracts, ID photos, medical records — in cloud storage
  • Anyone who assumed that database-level RLS also protects files in storage

If your app handles file uploads and you haven't explicitly verified your storage bucket settings, real project setups often end up more permissive than intended — through convenience choices during development or AI-generated defaults.


What Founders Experience

  • File uploads work correctly. Users upload files, the app stores them, the right users see the right files. The UI correctly scopes file visibility.
  • The storage layer is invisible. Founders configure database tables, write RLS policies, set up auth. Storage buckets are a separate system with separate configuration — and they're easy to overlook.
  • File URLs are predictable. AI tools often generate sequential or user-ID-based file naming: uploads/user_123/document.pdf. Anyone who knows the pattern can enumerate files by changing the ID.
  • Discovery comes from outside. A security researcher, a curious user, or an automated scanner finds publicly accessible files. By then, the files may have been public since the bucket was created.

What's Actually Happening

Supabase Storage buckets have two modes: public and private.

Public buckets serve files to anyone with the URL. RLS policies on the storage schema are silently ignored for read operations on public buckets. This is by design — "public" means public.

Private buckets require a valid user session and respect RLS policies. Files are only accessible through signed URLs or authenticated requests.

Two patterns create the exposure:

1. Bucket Created as Public

AI tools sometimes create storage buckets as public for simplicity — it avoids permission errors during development. Or the developer creates the bucket in the Supabase dashboard and selects "public" without understanding the implication.

Once the bucket is public, every file uploaded to it is publicly accessible by URL. RLS policies defined on the storage.objects table do not restrict read access to public buckets.

2. Predictable File Naming Increases Exploitability

A public bucket is the primary failure. Predictable file naming is a secondary amplifier that increases the scale and ease of exploitation.

Even without a directory listing, predictable file names make enumeration trivial. If invoices are stored as invoice_1.pdf, invoice_2.pdf, invoice_3.pdf, an attacker can download the entire collection by incrementing the number. If user files are stored under uploads/{user_id}/, and user IDs are sequential or guessable, every user's files are accessible.

Predictable naming is a risk factor beyond public buckets too — it can worsen damage from signed URL mishandling or other access control weaknesses. But combined with a public bucket, it makes every file in the bucket effectively browsable.

One documented case found a SaaS storing PDF invoices in a public bucket with names like invoice_1234.pdf. Thousands of customer invoices — with names, addresses, and payment details — were downloadable by anyone who enumerated the IDs.


What This Puts at Risk

Sensitive document exposure. Invoices, contracts, ID photos, medical records, legal documents — any file type stored in a public bucket is accessible. The sensitivity depends on what your users upload, but the exposure mechanism is the same.

Regulatory liability. Publicly accessible ID photos or medical records trigger specific regulatory obligations under GDPR, HIPAA, and other frameworks. File exposure is often treated as a data breach requiring notification.

Customer trust. Users who upload personal documents expect them to be private. Discovering that their files were publicly accessible — even if never accessed by an unauthorized party — erodes trust in a way that's difficult to recover from.


How Trust Score Detects It

AUTH-02: RLS enabled on all tables is the closest automated check — it verifies that database-level access control is enforced. Storage bucket configuration follows the same foundational principle (every data store needs access control), but bucket public/private settings are project configuration, not a code pattern that automated scanning can directly detect.

This makes storage exposure an area where the Launch Readiness Assessment adds the most value. The LRA includes manual review of storage bucket settings, file access patterns, and naming conventions — beyond what an automated scan covers.


Real Incidents

Sequential invoice enumeration. A SaaS stored PDF invoices in a public storage bucket with filenames like invoice_1234.pdf. An attacker enumerated thousands of invoices containing customer names, addresses, and payment details — without authentication.

Tea App — 72,000 images leaked (2026). 13,000 government ID photos and 72,000 user images were publicly accessible. The breach showed how rapid deployment without proper access controls on storage can expose sensitive user files at massive scale. Covered by BBC and NBC News.

Lovable-generated apps — storage not separately secured. In audits of AI-generated apps, storage bucket configuration was consistently overlooked. Developers configured RLS on database tables but left storage buckets in their default state — often public.


Detection: How to Check Your Own App

Check 1: Are your storage buckets public or private?

In the Supabase Dashboard, go to Storage → check each bucket's settings. Look for the public/private toggle.

Interpretation: Any bucket marked "public" that contains user-uploaded content is accessible to anyone with the file URL. RLS policies do not restrict read access on public buckets.

Check 2: Can you access files without authentication?

Try accessing a file URL directly in an incognito browser (no session):

https://YOUR-PROJECT.supabase.co/storage/v1/object/public/YOUR-BUCKET/some-file.pdf

Interpretation: If the file downloads without authentication, the bucket is public and files are accessible to anyone.

Check 3: Are file names predictable?

Review how your app names uploaded files. If filenames include sequential IDs, user IDs, or predictable patterns, an attacker can enumerate all files in the bucket without knowing specific URLs.

Interpretation: Predictable naming combined with a public bucket means every file is effectively browsable. Using UUIDs or random strings for filenames reduces enumeration risk, but UUIDs are not access control — they make guessing harder, not unauthorized access impossible. For sensitive files, the primary fix is a private bucket with authenticated access. Random naming is a useful secondary measure.


Related Launch Risks


FAQ

Our database has RLS. Doesn't that protect storage too?

No. Supabase Storage has its own access control, configured separately from database RLS. You can have perfect RLS on every table and still have a public storage bucket. The two systems are independent — configuring one does not automatically protect the other.

What's the difference between a public and private bucket?

A public bucket serves files to anyone with the URL. A private bucket requires authentication — files are accessed through signed URLs or authenticated API requests that respect RLS policies. For any bucket containing user-uploaded content, private is almost always the correct setting.

We use UUIDs in file URLs. Isn't that enough security?

UUIDs make enumeration harder, but they don't prevent access. If a UUID-based URL is shared, cached, or logged, anyone with the URL can access the file. UUIDs are an obfuscation layer, not an access control layer. For sensitive files, use private buckets with authenticated access.

How do we migrate a public bucket to private?

Changing a bucket from public to private in the Supabase dashboard is straightforward. But you'll also need to update your application code to generate signed URLs for file access, since direct public URLs will stop working. Test the change in a staging environment before applying it to production.


Is This Happening in Your App?

Run a free Trust Score scan — 24 safety checks across auth, billing, and admin. Results in seconds.