How To

Firebase Security Rules Audit: The Misconfigurations That Expose Your App

Seven Firebase misconfigurations across rules, perimeter and credentials, from test-mode rules that became 'if true' to App Check left in monitoring. Each comes with a console path or command to detect it, and a walk-through of judging one ambiguous rules hit.

11 min read
Cover Image for Firebase Security Rules Audit: The Misconfigurations That Expose Your App

Firebase Security Rules Audit: The Misconfigurations That Expose Your App

Someone spun up your Firestore in test mode two years ago to ship a demo. The console handed them a rule that expired after 30 days, the app broke on that date, and to unbreak it someone pasted in allow read, write: if true. That line is still in production. Every document in your database is readable and writable by anyone who knows the project ID, and the project ID ships in your JavaScript bundle.

This is the most common finding in a Firebase security rules audit, and monitoring almost never catches it, because nothing errors. The app works. Data flows. The rules are simply doing nothing. Firebase puts security almost entirely into declarative rules files that few teams review after launch, so misconfigurations sit quietly until someone scrapes the database or runs up your bill.

This guide walks the misconfigurations that actually matter: what each one is, why it happens, one console path or command to detect it, and the real consequence. Run the checks today. They take about an hour on a typical project.

This is part one of a three-part series. Part two is a DIY Firebase security audit using only native tools; part three covers continuous auditing with an AI agent.

01Rules, 1 to 4

What a request may do

Open test-mode rules, auth without ownership, open Storage, cascading RTDB grants.

02Perimeter, 5 to 6

Who gets to ask

App Check left in monitoring, open sign-up, stale authorized domains.

03Credentials, 7

What a key unlocks

The web API key is not a secret. A committed service-account key is.

Seven misconfigurations across three layers. Each layer fails silently: the app keeps working.

Why Firebase misconfigurations survive so long

In a traditional backend, an access-control bug lives in server code that goes through review, tests and a deploy pipeline. Firebase moves that boundary into rules files, and the usual safety nets don't follow.

The client talks to the database directly. There's no API layer of your own between the browser and Firestore. Whatever the rules allow, any HTTP client can do, with nothing but the public config.

Rules fail open quietly. An over-broad rule produces no error, no log line and no failed test. The only signal is data leaving, and nothing in the default setup watches for that.

Rules deploy outside the code path. firebase deploy --only firestore:rules from a laptop, or a paste into the console editor, replaces the active ruleset in seconds. Neither path requires a pull request.

Each product has its own rules. Firestore, Cloud Storage and the Realtime Database each have a separate ruleset, in two different languages. Locking down one says nothing about the other two.

Prototypes become production. Projects created for a demo or a hackathon keep their test-mode posture, their open sign-up and their stale domains long after real users arrive.

01Day 0

Test mode

The console writes an open rule with a 30-day expiry.

02Day 30

App breaks

The rule expires and every read and write is denied.

03Day 30, later

if true

An engineer under pressure pastes in a rule that never expires.

04Day 700

Still live

Nothing errors, so nobody looks. The project ID ships in the bundle.

The most common Firebase finding is a release nobody reviewed. Step three is where it could have been caught.

1. Test-mode rules that never got locked down

What it is. When you create Firestore, Storage or the Realtime Database in "test mode," the console generates a time-boxed open rule. For Firestore it looks like allow read, write: if request.time < timestamp.date(2026, 8, 12);. It grants unrestricted access until that date, then denies everything. The trap is what happens on expiry: the app breaks, and the fastest fix an engineer under pressure reaches for is allow read, write: if true;, which never expires and never says no. A close cousin is the same timestamp rule with the date hand-edited years into the future.

Why it happens. Test mode is the path of least resistance during prototyping, and "we'll lock it down before launch" is a task with no owner. The expiry breakage arrives long after the person who created it has moved on.

How to detect it. Open Firebase Console → Firestore Database → Rules and read the rules top to bottom. Any allow read, write: if true, any if request.time < timestamp.date(...), or a catch-all match /{document=**} granting broad access is a finding. Repeat for Storage → Rules and Realtime Database → Rules. If the rules are in your repo, a first pass is one grep (part two shows how to pull the deployed version, which can differ):

grep -nE 'if\s+true|request\.time\s*<' firestore.rules storage.rules

Typical consequence. Full read means anyone can scrape every record: user PII, private messages, internal config. Full write means anyone can overwrite or wipe your collections. This is the pattern behind most public "misconfigured Firebase database" disclosures.

2. Firestore rules that check auth but not ownership

What it is. The second most common Firestore rules mistake looks responsible at a glance: allow read, write: if request.auth != null;. It requires a signed-in user, but it doesn't check which user. Any authenticated account, including one an attacker creates in seconds through your own public sign-up, can read and write every other user's documents.

Why it happens. "Require login" feels like the security control. The jump from "is authenticated" to "owns this specific document" needs per-collection ownership logic that's easy to defer.

How to detect it. In Firestore Database → Rules, find every rule guarded only by request.auth != null with no comparison against a document field or path segment. A correct per-document rule ties identity to the resource:

match /users/{userId} {
  allow read, write: if request.auth != null && request.auth.uid == userId;
}

For documents that carry an owner field, the read side checks the stored document and the create side checks the incoming one:

match /orders/{orderId} {
  allow read, update, delete: if request.auth != null
    && request.auth.uid == resource.data.ownerId;
  allow create: if request.auth != null
    && request.auth.uid == request.resource.data.ownerId;
}

If your rules never reference request.auth.uid alongside userId or resource.data.ownerId, ownership isn't enforced.

Typical consequence. Horizontal privilege escalation: user A reads and edits user B's data. Because sign-up is usually open, "authenticated" isn't a meaningful barrier. The attacker simply registers. With anonymous auth enabled, they don't even need an email.

3. Cloud Storage rules left wide open

What it is. Storage rules are a separate ruleset from Firestore, and they're often forgotten. A bucket with allow read, write: if true;, or the read-open, write-authenticated variant, lets anyone download every uploaded file and, worse, upload arbitrary content that counts against your storage and egress bill.

Why it happens. Teams lock down Firestore and assume Storage inherited the same posture. It didn't. Each product has its own rules file.

How to detect it. Open Firebase Console → Storage → Rules. Look for allow read or allow write without an auth or ownership condition, and confirm write paths are scoped to a per-user prefix. A safe pattern scopes uploads to the owner and caps the size:

match /user-uploads/{userId}/{fileName} {
  allow read: if request.auth != null && request.auth.uid == userId;
  allow write: if request.auth != null && request.auth.uid == userId
    && request.resource.size < 10 * 1024 * 1024;
}

Typical consequence. Public read exposes user-uploaded documents, avatars, receipts and ID scans. Public write turns your bucket into free hosting for whatever strangers upload, with a bandwidth bill you didn't budget for.

4. Realtime Database rules anti-patterns

What it is. The Realtime Database uses a different, JSON-based rules language (.read and .write expressions) with its own anti-patterns. The classic is a top-level ".read": true or ".write": true, which cascades to every child node. A subtler one: rules that grant access at a high path (/users) instead of scoping to /users/$uid, so any user sees the whole tree.

Why it happens. RTDB rules cascade downward. Granting access to a parent grants it to all children, and a deeper rule can't take it back. That inheritance is easy to get wrong, and legacy projects that predate Firestore often still run RTDB with rules nobody has revisited.

How to detect it. Open Firebase Console → Realtime Database → Rules. A correctly scoped rule looks like:

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    }
  }
}

Any ".read": true or ".write": true, or an auth != null check not paired with a $uid match, is a finding.

Typical consequence. A single open .read at the root exposes the entire database in one request to https://your-db.firebaseio.com/.json. Open .write allows wholesale data destruction.

5. Missing App Check

What it is. Security rules decide what a request may do. They can't verify the request came from your actual app. App Check closes that gap by attesting that traffic comes from your genuine web or mobile client (via reCAPTCHA, App Attest or DeviceCheck, or Play Integrity). Without it, anyone can hit your Firebase backends directly with a script, using nothing but the public config, and your rules are the only thing between them and your data.

Why it happens. App Check is opt-in. Teams reasonably fear that enforcing it will break existing clients, so it stays in monitoring, or never gets enabled at all.

How to detect it. Open Firebase Console → App Check. Check each product (Firestore, Storage, Realtime Database, Authentication) for its enforcement state. "Unenforced" or monitoring-only means App Check isn't blocking anything.

Typical consequence. Automated abuse: scripted scraping of data your rules allow to authenticated users, credential stuffing against your Auth endpoints, and quota or billing abuse that rules alone can't stop.

6. Unenforced Firebase Authentication settings

What it is. Firebase Authentication has protections that aren't all on everywhere. Email enumeration protection, which stops attackers from probing which addresses have accounts, is on by default for projects created since September 2023, but older projects often still have it off. Unlimited public sign-up is another gap: if your Firestore rules trust "any authenticated user," an open sign-up flow is a self-service key.

Why it happens. Auth is configured once during setup and rarely revisited. The defaults optimize for a frictionless first launch, not a hardened production posture.

How to detect it. Open Firebase Console → Authentication → Settings and review the user-actions and abuse-prevention options. Under Sign-in method, confirm only the providers you actually use are enabled, with anonymous sign-in getting the hardest look. Then check Authentication → Settings → Authorized domains: stale entries (a preview domain, a contractor's tunnel) let auth flows complete on hosts you no longer control.

Typical consequence. Account enumeration feeds targeted phishing. Unbounded sign-up combined with weak rules hands attackers the "authenticated" status your rules trust. Stale authorized domains enable OAuth redirect abuse.

7. The exposed web API key that isn't actually a secret

What it is. Firebase web API keys appear in every client bundle, and security scanners flag them constantly. The sober reality: a Firebase web API key is not a secret. It identifies your project to Google's servers. It doesn't grant access to data. Access is governed by Security Rules and App Check. A leaked web API key, on its own, doesn't expose your database.

Why it matters anyway. The key isn't harmless. It can call Identity Toolkit endpoints, for example to create accounts against your project, which is why unrestricted sign-up plus a public key enables the abuse in sections 2 and 6. And a leaked service account key (a JSON file for the Admin SDK) is a different and genuinely critical exposure: the Admin SDK bypasses all rules.

How to detect it. In the Google Cloud Console → APIs & Services → Credentials, review each API key's application restrictions (HTTP referrers for web) and API restrictions. Confirm no service-account JSON files live in your repo:

git log -p --all | grep -n '"private_key"'

That's a fast first pass, not a full secrets scan, and it reaches every branch in history.

Typical consequence. Chasing the web API key wastes an audit. Missing a committed service-account key is a full compromise. Spend your attention accordingly.


Why a one-time audit isn't enough

Every fix above holds only until the next release. Rules are code, and code regresses: a teammate widens a match block to unblock a feature, a new collection ships without an ownership check, someone switches App Check back to monitoring to debug a client and forgets to re-enforce it. None of these throw an error. The app keeps working, and the exposure lands quietly between audits.

Here's how one of those looks when you finally find it. A grep of the deployed Firestore rules turns up allow read: if request.auth != null; on orders. Three readings fit:

  • Intended. orders holds shared, non-personal data. Then the documents have no owner field, and the product team can tell you why it's open.
  • Missing ownership. orders is per-user. Then documents carry an ownerId, the rule never compares it, and the Rules Playground lets a fabricated UID read someone else's order.
  • Effectively public. Either of the above, plus anonymous sign-in enabled. Then "any signed-in user" means anyone with the project ID.

Telling them apart takes a document sample, a Playground run and a look at the sign-in providers. Part two walks through each. The point here is that the grep is the easy part. The judgment is what makes it an audit, and it has to happen again after every release.

Where this series goes next

Start manual. Part two walks through a full DIY audit using the Firebase CLI, the Rules Playground and the Emulator Suite, including the rules tests that stop a regression in CI. Know that a point-in-time pass ages the moment the next rules release lands.

Part three makes the audit continuous. CloudThinker's security agent connects to your Firebase projects read-only, inventories every app, audits Firestore, Storage and Realtime Database rules, and flags a rules release that widens access the hour it lands. It proposes the corrected rules and acts within Auto Mode, so you decide what runs on its own and what waits for approval.

Try CloudThinker free with 100 premium credits and no card required, or keep going with the DIY audit guide.

firebasesecurityfirestoreappsecuritycloudsecuritysecurityaudit