Open Storage Buckets: The #1 Firebase Leak

Firebase Storage security is where the worst leaks happen. Learn how open buckets exposed 72,000 IDs, how to write owner-scoped rules, and how to check yours.

Barret8 min read

Your app lets people upload things — profile photos, documents, maybe ID images. Your AI builder wired up Firebase Storage to hold them, and it works. But somewhere in the back of your mind is a quiet worry: who, exactly, can see those files? Is there anything stopping a stranger from listing every image your users ever uploaded?

That worry is the right one to have, because Firebase Storage security is where the most damaging Firebase leaks actually happen. Not the much-hyped API key — open Storage buckets. They're the most common serious mistake in AI-built Firebase apps, and they've already caused one of the largest breaches in this whole space.

This post shows you what an open bucket is, the default rules that leave it open, how to write owner-scoped rules that lock it down, how to check what's publicly readable right now, and how to serve private files safely with signed URLs.

⚡ TL;DR

  • Firebase Storage has its own rulebook, separate from your database. Locking the database does nothing for your files.
  • The default test-mode rule (allow read, write: if true;) leaves your entire bucket readable and writable by anyone — this is the #1 real Firebase leak.
  • Fix it by scoping every file path to its owner (request.auth.uid), and serve private files with time-limited signed URLs instead of public ones.

Why Storage is the most dangerous part of Firebase

Here's the thing people miss: Firebase Storage has a completely separate set of rules from your database. Two different rulebooks. You can write airtight Security Rules for your Firestore database, feel safe, and still have a Storage bucket wide open to the internet — because you locked one door and never touched the other. (For how Storage rules fit alongside the other Firebase controls, see the cornerstone, Firebase security for AI-built apps.)

And Storage is the more dangerous one to get wrong, because of what lives there. Your database holds records — names, IDs, references. Your Storage bucket holds the actual files: selfies, uploaded passports, medical documents, private photos. When a database leaks, an attacker gets rows; when a bucket leaks, they get the files themselves, ready to download in bulk. An open bucket means anyone who knows or guesses the URL can list and download every file in it. No login, no "hack" — only a public address and a script. That's not hypothetical; it's exactly how the worst Firebase breach on record played out.

What "open bucket" really means: the Tea app

The clearest cautionary tale is the Tea app. In July 2025, an unsecured legacy Firebase storage bucket exposed roughly 72,000 images, including about 13,000 selfies and government IDs. A second breach then exposed more than 1.1 million private direct messages. The fallout is now consolidated class-action litigation in California. These weren't database rows — they were the actual photos of people's faces and scans of their government IDs, the most sensitive files an app can hold, sitting in a bucket anyone could reach.

Here's the part every Firebase founder needs to internalize: the root cause was not the public API key. Nobody "cracked" anything. The cause was an open Storage bucket — missing or permissive rules on files that should have been locked to their owners. Every founder who panics about their apiKey in their frontend is looking at the wrong thing; the Tea app shows where the real danger lives. We break it down soberly in the Tea app breach.

The default rules that leave your bucket open

So how does a bucket end up open in the first place? Usually because an AI builder left the default in place, or because the project was put in test mode, which sets open access on a 30-day timer.

The dangerous rule looks like this:

// DANGER: anyone on the internet can read and write every file
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;
    }
  }
}

That if true means "yes to everyone, always." The {allPaths=**} means "every file, in every folder." Together they say: the whole bucket is open to the public, for reading and writing.

A slightly less obvious variant only requires that someone be logged in:

// STILL RISKY: any logged-in user can read and write ANY file
match /{allPaths=**} {
  allow read, write: if request.auth != null;
}

This feels safer because it requires authentication. But it lets any signed-up user read and write everyone else's files — including files they don't own. In an app where anyone can register, that's barely a lock at all. The missing piece is an ownership check.

How to write owner-scoped Storage rules

The fix is to scope every file to its owner: store each user's files under a path that contains their user ID, then write a rule that only lets that same user touch that path. Say you store profile photos at users/<uid>/profile.jpg. The rule that locks it down:

// Each user can only read and write files under their own folder
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /users/{userId}/{fileName} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;
    }
  }
}

The line doing the real work is request.auth.uid == userId. It checks that the logged-in user's ID matches the folder name — so a user can reach users/<their-own-id>/... and nothing else, and anyone trying to read another person's folder gets denied.

You'll often want different rules for reading versus writing. A common pattern: let any logged-in user view an image (profile photos shown across the app), but only let the owner upload or replace their own, and cap file size and type so nobody uploads a 2 GB or malicious file:

// Anyone logged in can view; only the owner can write; images only, max 5 MB
match /users/{userId}/{fileName} {
  allow read: if request.auth != null;
  allow write: if request.auth != null
               && request.auth.uid == userId
               && request.resource.size < 5 * 1024 * 1024
               && request.resource.contentType.matches('image/.*');
}

Adjust the read line to your needs. If the files are truly private (documents, IDs), make reading owner-only too, by adding the same request.auth.uid == userId check to allow read.

How to check your own app

Before you change anything, find out what's publicly readable right now. This takes a few minutes and no code.

  1. Open the Firebase Console → Storage → Rules. Read them out loud. If you see allow read, write: if true;, or a banner saying you're in test mode with an expiry date, your bucket is open to the internet right now.
  2. Watch for the authenticated-but-unscoped trap. If the rule is allow read, write: if request.auth != null; with {allPaths=**} and no request.auth.uid check, any logged-in user can reach everyone's files. Still a leak.
  3. Try to reach a file without logging in. Open one of your file's download URLs in a private/incognito window where you're not signed in. If it downloads, that file is public to anyone with the link.
  4. Watch the requests your app makes. Open the browser's dev tools → Network tab and watch the Storage requests as you use the app. If you can fetch files that aren't yours with only the public config, so can anyone else.

Not sure if your app has this exact issue?

Run a free, read-only scan of your live app — no install, results in under a minute.

Scan my app free →

Signed URLs: how to serve private files safely

Owner-scoped rules solve the "who's allowed" question for files accessed through your app while a user is logged in. But sometimes you need to hand someone a direct link to a private file — an email attachment link, a temporary download, a file shown to a user who shouldn't get permanent access. For that, use a signed URL.

A signed URL is a time-limited link that grants access to one specific file for a short window — say, 15 minutes — then stops working. It lets you share a private file without making the bucket public. The contrast that matters: a public download URL works forever for anyone who has it, so if it ever leaks (a forwarded email, a screenshot, a logged URL) the file is exposed permanently; a signed URL expires, so a leak is far less damaging because the link is already dead.

Rule of thumb: keep your bucket private with owner-scoped rules, and when you need to share a private file outside the normal logged-in flow, generate a short-lived signed URL rather than flipping the file to public. Generating them is a server-side operation, done with the Admin SDK on a backend you control — not in client code.

The fix, in priority order

If your check turned up problems, here's the order that reduces risk fastest:

  1. Close any if true Storage rules immediately. This is the open-to-the-world case — the Tea app scenario, the one that ends up in the news.
  2. Add ownership checks. Replace request.auth != null blanket access with request.auth.uid == userId scoped to per-user paths.
  3. Lock reads on truly private files. For IDs, documents, and anything sensitive, make allow read owner-only, not any-logged-in-user.
  4. Switch permanent public links to signed URLs for private files, so a leaked link expires.
  5. Constrain uploads with size and content-type checks so the write path can't be abused.

If you build with an AI tool, paste a prompt like: "Review my Firebase Storage Security Rules. Replace any allow read, write: if true or test-mode rules. Scope every file path to its owner so a user can only read and write files under their own request.auth.uid. For sensitive files, make reads owner-only too, and add a size and content-type limit on writes. Do not make the bucket publicly readable."

How this compares to Supabase Storage

If you've used Supabase instead of (or alongside) Firebase, the storage failure mode is the same shape with different defaults. Supabase organizes files into buckets that can be public or private, and the classic mistake there is a bucket left public by default, plus missing access policies on private buckets — the direct parallel to Firebase's open bucket. Same lesson on both: files are the crown jewels, and the rule that scopes them to their owner is what stands between you and a breach. We cover the Supabase side in Supabase Storage buckets: the public-by-default mistake.

FAQ

Why are Firebase Storage rules separate from database rules?

Because Firebase treats files and database records as two different services, each with its own rulebook. Your Firestore rules govern documents; your Storage rules govern files. Securing one does nothing for the other. This separation is exactly why people lock their database, feel safe, and leave their file bucket wide open — which is how the most damaging Firebase leaks happen.

How do I make a Firebase Storage bucket private?

Open the Firebase Console → Storage → Rules and replace any allow read, write: if true (or test-mode) rule with one that requires authentication and checks ownership — request.auth.uid == userId against a per-user file path. For files that should be truly private, make the read rule owner-only too. The bucket isn't "public" or "private" as a single switch; it's the rules that decide.

What's a signed URL and when should I use one?

A signed URL is a time-limited link to a single private file that expires after a set window. Use one when you need to share a private file outside your normal logged-in flow — a download link, an email attachment — without making the file or the bucket permanently public. If a signed URL leaks, it's far less damaging because it's already expired.

The bottom line

Forget the API-key panic. The Firebase mistake that actually leaks data — the one that put 72,000 images and 13,000 government IDs in the open — is an unsecured Storage bucket. Lock every file to its owner with request.auth.uid checks, keep sensitive reads owner-only, and use short-lived signed URLs for private sharing. An attacker can list an open bucket in minutes with nothing but your public config; find that open bucket first, and keep checking, because every new upload feature your AI adds is a new bucket and a new chance to leave the door open.

Find your gaps before an attacker does.

Is My Site Hackable? scans your deployed app for the exact issues in this article — exposed keys, missing RLS, open buckets — and tells you what's real and what's a false alarm.

Run a free scan →