Your storage bucket is public — so are all of its images
The images were supposed to be semi-private — avatars, receipts, uploads visible to their owners and staff. Then someone notices the URL in the address bar carries no signature and no expiry: the bucket is public, and every object in it downloads from a plain link. Nothing was hacked. A public bucket publishes, by definition, and the definition is doing exactly what it says.
Storage exposure is quieter than table exposure because there is no JSON to stumble over — just URLs that work. This page covers what the public flag actually serves, how to confirm a bucket's posture in seconds from inside and out, what making it private does and does not undo, and how the setting gets watched so the next temporary public bucket does not become permanent.
A note on tone before the mechanics: plenty of projects run a public bucket responsibly for genuinely public assets. The purpose here is not to condemn the flag but to make its reach concrete — what it serves, to whom, and how quickly intent and configuration part ways once uploads start accumulating in a bucket nobody re-reads.
Rules that check this
- highStorage bucket is public
PUBLIC_BUCKET_EXPOSURE
What the public flag serves
A bucket marked public is served through an unauthenticated route: the object path appended to /storage/v1/object/public/ returns the bytes to whoever holds the URL, with no key, no session and no policy consultation. The storage service does not distinguish between marketing assets and scanned documents; the flag applies to the bucket as a whole.
Nobody will guess the paths is the load-bearing assumption, and it fails slowly. Paths appear in browser dev tools, in referral headers, in screenshots, in links shared with colleagues; once an application serves objects publicly, their addresses circulate beyond anyone's bookkeeping. Public means published, and published means retained by anything that found the link once.
Confirm a bucket posture from both sides
From the outside, take any object path you can construct and request it — a 200 with bytes means the bucket serves strangers; an error means it refuses them. Try an object you would consider sensitive rather than the logo, for obvious reasons:
curl -s -o /dev/null -w "%{http_code}\n" \
"https://your-project.supabase.co/storage/v1/object/public/uploads/invoice-january.pdf"Read the metadata directly
From the inside, bucket metadata answers directly; the query below lists every bucket with its public flag, and it reads only metadata — the same surface RowShield consults, never object contents. If you keep more than a couple of buckets, expect this to surface one or two nobody remembers creating:
SELECT id, name, public FROM storage.buckets ORDER BY public DESC, name;
Making it private, honestly
Any row with public set to true is a decision to review rather than a bug to panic about — some buckets are meant to be public, and serving logos from one is ordinary practice. The question is whether everything currently inside deserves the audience the flag gives it, and the honest answer for mixed buckets is usually no.
Flipping the bucket to private stops the unauthenticated route serving new requests, and applications switch to signed URLs — short-lived links minted server-side per request. Plan both halves as one release, because a private bucket with unsigned client URLs breaks galleries and avatars immediately:
-- server-side: mint a short-lived link instead of a permanent one
const { data } = await supabase
.storage
.from('uploads')
.createSignedUrl('invoice-january.pdf', 600);Caveats, then continuous watching
Two honest caveats. Links already shared, cached by intermediaries or saved by recipients do not expire when the flag flips; treat previously public objects as disclosed if their sensitivity warrants it. And signed URLs protect the door, not the room: anyone with valid access can save the file, so expiry windows should match genuine need rather than habit.
Bucket flags drift like everything else — a feature wants quick sharing, someone flips the toggle, and nobody revisits. RowShield reads bucket metadata on every scan and raises PUBLIC_BUCKET_EXPOSURE for public buckets, so a flip arrives as a finding naming the bucket rather than as a surprise months later; alerts fire on transitions — created, regressed or resolved — so a stable configuration stays quiet.
RowShield is an independent product, unaffiliated with and not endorsed by Supabase; it reads catalog and bucket metadata only — never object contents — and treats Supabase as a descriptive reference. Run a free audit — no account — at rowshield.dev/audit to see which buckets currently answer strangers.
Frequently asked
- Are public buckets always wrong?
- No. Buckets holding genuinely public assets are ordinary, and keeping them separate from user uploads is good practice. The finding-worthy state is sensitive content sitting in a bucket whose flag gives it a worldwide audience.
- Will privatising break my live app?
- It will, briefly and predictably, wherever the client builds plain object URLs. Ship signed-URL handling in the same release, and the break window shrinks to the deploy itself rather than an open-ended outage.
- What about objects that were downloaded already?
- Copies held by caches and recipients are beyond any flag; privatising stops future serving, not past retention. If previously public material was sensitive, treat it as disclosed and judge rotation at the object level.
Check your project in about ten seconds
Paste a URL. No signup, no writes, nothing stored.
Run the free audit