Firebase security rules: examples that leak data, and rules to paste instead

Firebase security rules decide who can read and write your Firestore, Realtime Database and Storage data. Your Firebase config ships in your app's JavaScript, so the rules are all that stands between a stranger and your data. allow read, write: if true, a test mode date or request.auth != null alone all let strangers in. Tie each rule to the caller's uid, or to a role your server set.

The team behind Keybrum's attacksUpdated 14 min read

In short

  • Your Firebase config, API key included, is public in your JavaScript. Firebase says the key only identifies your project, so the rules alone decide who reads your data.
  • allow read, write: if true and the test mode date rule both open everything they cover. The date protects nothing before it passes, and breaks the app after.
  • request.auth != null means any account, and anyone can make one, anonymous ones included. Compare request.auth.uid with the owner saved in resource.data.
  • Allows add up: one catch-all rule on a recursive wildcard overrides every careful rule in the file. In the Realtime Database, a parent's rule overrides its children.
  • Test as a visitor with no account and as a second user, in the Rules Playground and with emulator unit tests, before every deploy.
On this page

How Firebase security rules work

Firebase security rules are the access rules for three products: Cloud Firestore, the Realtime Database and Cloud Storage. Each product has its own set, and every request from a browser or a mobile app is checked against them before any data moves. A request that no rule allows is denied.

They carry all the weight because everything else is public. Your app's Firebase config, API key and project ID included, ships in its JavaScript. Firebase's own security checklist says these API keys are not secret: they only identify your project, and access is decided by Security Rules, IAM and App Check. Anyone who reads your bundle can call Firestore directly, without going through your screens. The Firebase API key guide explains why hiding the key would not help.

javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // match: which documents this block covers
    match /notes/{noteId} {
      // allow: which operations, and the condition that must be true
      allow read, delete: if request.auth != null
                          && request.auth.uid == resource.data.ownerId;
    }
  }
}
A Firestore rule. Cloud Storage uses the same language. The Realtime Database uses JSON, covered further down.
PartWhat it means
match /notes/{noteId}The path the block applies to. {noteId} is a wildcard for one document ID, usable in conditions. A recursive wildcard matches a whole subtree, see below.
allow read, writeThe operations granted. read splits into get (one document) and list (queries). write splits into create, update and delete.
requestThe incoming call. request.auth holds the signed-in user (uid and token claims), or null for a visitor with no account. request.resource.data is the document as it would be after the write.
resourceThe document as it is stored now. resource.data.ownerId is the saved owner, which the caller cannot rewrite on its own.

Two rules of evaluation explain most leaks. First, allows add up: when several match blocks cover the same document, access is granted if any one of them is true, so a narrow rule can never take back what a broad one gave. Second, rules are not filters. Firestore checks a query against every document it could return and rejects the whole query if one of them would fail, instead of quietly leaving it out.

Rules judge the requests that come from your users' browsers and phones. The Firebase Admin SDK, and Cloud Functions that use it, reach your data whatever the rules say. That is why anything a client must not do belongs in server code.

Firebase rules examples that leak data

Firebase's own documentation lists most of these patterns as insecure, and they still reach production, in generated code and in leftover quick starts. Here is what each one lets a stranger do.

RuleWho gets inWhat a stranger can do
allow read, write: if true;Anyone, no account neededRead, change and delete every document the block covers
if request.time < timestamp.date(...) (test mode)Anyone, until the dateThe same, then the app breaks when the date passes
if request.auth != null;Anyone with an account, anonymous ones includedSign up, then read or write every user's data
request.auth.uid == request.resource.data.ownerId on updateAny signed-in userTake over another user's document by writing their own uid into it
A catch-all match /{document=**}, or {allPaths=**} in StorageWhoever the condition lets in, on every pathReach every collection or file, whatever the narrower rules say
request.auth.token.email.matches('.*@company[.]com')Anyone who signs up with that address, unverifiedPass as staff without owning the mailbox

Open to everyone: allow read, write: if true

javascript
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;   // anyone, any document
    }
  }
}

Firebase's documentation warns that with rules like these, anyone who guesses your project ID can steal, modify or delete the data, and the project ID sits in your JavaScript. allow read: if true on its own, with no write, is fine for data you would publish anyway, like a public catalogue, scoped to that one collection. On users, orders, messages or anything holding an email address, it is a leak.

Test mode: open until a date, then broken

javascript
// Test mode: open to everyone until the date
match /{document=**} {
  allow read, write: if request.time < timestamp.date(2026, 12, 7);
}

When you create a Firestore database in test mode, you start with a rule like this one: it compares the time of each request with a fixed date. Firebase's quickstart says test mode "allows anyone to read and overwrite your data", and until the date it is exactly like if true. The starter rules file in the Firebase CLI sets the date 30 days ahead; once it passes, no rule matches and every client request is denied. For the Realtime Database, Firebase documents an email alert before its test mode rules start denying all requests. Apps often ship while the date is still ahead, and when the app breaks on the day, the quickest edit is to move the date a year forward. Treat any request.time < rule that covers the whole database as open.

Signed in is not the same as allowed: request.auth != null

javascript
match /users/{userId} {
  allow read, write: if request.auth != null;   // any account, anyone's data
}

request.auth != null asks a single question: does the caller have an account? In most apps anyone can create one, and Firebase's security checklist points out that anyone could create an anonymous account in your project. So this rule means "any stranger after a sign-up", and with a wildcard on users, every user's profile. It is the database version of an IDOR vulnerability: the app shows each user their own data, while the database hands anyone everyone's.

The owner check that only reads the incoming data

javascript
// Leaks on update: the client chooses what ownerId says
allow write: if request.auth.uid == request.resource.data.ownerId;

request.resource.data is what the client sends, so the client decides what ownerId says. On a create, that is the check you want. On an update, any signed-in user can overwrite someone else's document by putting their own uid in it. Check the stored version as well: resource.data.ownerId is the owner as saved, and the caller cannot change it.

Recursive wildcards that cover more than you meant

{document=**} matches every document below it, in every subcollection. With rules_version = '2' it also matches zero segments, so match /users/{userId}/{document=**} covers the user document itself as well as everything under it. Because allows add up, a catch-all block left at the bottom of the file, often the old test mode rule, cancels every careful rule above it. Find every recursive wildcard in your rules and check what each one opens.

Firebase rules examples that hold

The fix is the same idea in every product: compare the caller's uid, or a role your server set, with the data. Start from closed and open only what the app uses.

Owner only

javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // One profile per user, stored under their uid
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    // Documents that carry their owner's uid in a field
    match /notes/{noteId} {
      allow read, delete: if request.auth != null
                          && request.auth.uid == resource.data.ownerId;
      allow create: if request.auth != null
                    && request.auth.uid == request.resource.data.ownerId;
      allow update: if request.auth != null
                    && request.auth.uid == resource.data.ownerId
                    && request.resource.data.ownerId == resource.data.ownerId;
    }
  }
}
Anything no rule matches is denied. If the profile stores a role, a plan or credits, also limit which fields the user can change, as shown below.

Queries must then ask only for what the rules allow, for example where('ownerId', '==', user.uid). A query on the whole notes collection fails under these rules, because rules are not filters. When a coding agent runs into that permission error, the fix belongs in the query: loosening the rule to make the screen work reopens the leak.

Roles from custom claims

For admins, staff or paid plans, put the role in the user's token from your server with a custom claim. The client cannot change it.

javascript
// On your server, with the Admin SDK
import { getAuth } from "firebase-admin/auth";

await getAuth().setCustomUserClaims(uid, { admin: true });
javascript
match /reports/{reportId} {
  allow read, write: if request.auth != null && request.auth.token.admin == true;
}

A new claim reaches the user's ID token the next time one is issued, so the change is not instant. The claims payload is limited to 1,000 bytes: keep roles there and profile data elsewhere.

Roles from a document, and the trap that comes with it

You can also keep roles in Firestore and read them in the rule with get():

javascript
allow write: if request.auth != null
  && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';

This only works if users cannot edit their own role field. With the owner-only profile rule above, they can: a user updates their profile to role: 'admin', and every rule that trusts the field now trusts them. Limit the fields a user may write:

javascript
match /users/{userId} {
  allow read: if request.auth != null && request.auth.uid == userId;
  allow create: if request.auth != null && request.auth.uid == userId
                && request.resource.data.keys().hasOnly(['displayName', 'photoURL']);
  allow update: if request.auth != null && request.auth.uid == userId
                && request.resource.data.diff(resource.data).affectedKeys()
                     .hasOnly(['displayName', 'photoURL']);
}

Each get() or exists() is billed as a document read, even when the rule then rejects the request, and one request can make at most 10 of them (20 for multi-document reads, transactions and batched writes). Custom claims cost nothing per request, one more reason to prefer them for roles.

Validate what gets written

An owner check decides who writes. Validation decides what. Without it, a user can add fields your app trusts, like plan: 'pro' or credits: 99999, or store junk you pay for. Check the list of fields, their types and their sizes:

javascript
allow create: if request.auth != null
  && request.auth.uid == request.resource.data.ownerId
  && request.resource.data.keys().hasOnly(['ownerId', 'title', 'body'])
  && request.resource.data.title is string
  && request.resource.data.title.size() <= 200;

Realtime Database rules: JSON, and parents win

The Realtime Database uses JSON rules. .read and .write grant access, .validate checks the data, and keys starting with $ are wildcards. Conditions use auth (the signed-in user, or null), data (what is stored) and newData (what it would be after the write).

The trap is the cascade. A rule at a shallower path overrides the rules below it: once a parent grants read, a child cannot take it back. So ".read": true at the root opens everything, whatever the children say. .validate rules behave the other way round: they do not cascade, and every one of them must pass at every level for a write to go through.

json
{
  "rules": {
    ".read": true,
    "users": {
      ".read": false
    }
  }
}
The child rule is ignored: the root already allowed the read.
json
{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid",
        ".write": "auth !== null && auth.uid === $uid",
        "name": { ".validate": "newData.isString() && newData.val().length <= 100" },
        "$other": { ".validate": false }
      }
    }
  }
}
Nothing is granted at the root, so everything stays closed unless a deeper rule opens it. $other rejects any field you did not list.

Anonymous accounts pass auth !== null here too. To tell them apart, check the provider field of auth, which is anonymous for those accounts.

Cloud Storage rules: owners, sizes and file types

Storage rules use the Firestore language, matched against file paths in the bucket. The usual leak is the same open or auth-only rule as in Firestore, matched on {allPaths=**} instead of {document=**}, which serves every upload, ID scans and invoices included, to whoever asks. The usual fix keys each folder by its owner's uid and limits what can be uploaded:

javascript
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    // Files look like: user/<uid>/<file>
    match /user/{userId}/{fileName} {
      allow read, delete: if request.auth != null && request.auth.uid == userId;
      allow create, update: if request.auth != null && request.auth.uid == userId
                            && request.resource.size < 5 * 1024 * 1024
                            && request.resource.contentType.matches('image/.*');
    }
  }
}

Reads split in two here as well: get downloads one file and list lists a folder, and list only exists with rules_version = '2'. A public allow read covers both, so anyone can list a folder of user uploads and download each file. Without the size and type checks, a user can upload gigabytes you pay to store, or a file that is not the image your app expects.

How to test Firebase security rules before you deploy

  1. 1

    Read what is actually deployed

    In the Firebase console, open Firestore, Realtime Database or Storage and click the Rules tab: it shows the version that is live. If the project also has a rules file in the repository, edit in one place only: firebase deploy --only firestore:rules (or database, or storage) from an old file overwrites a fix made in the console.

  2. 2

    Try requests in the Rules Playground

    From the Rules tab, open Rules playground. Pick a read or a write, a document path, and who is asking: unauthenticated, an anonymous user, or a specific user ID. Run it as a visitor with no account, then as a user who does not own the document. Both should be denied.

  3. 3

    Write unit tests against the emulator

    The Firebase emulator runs your rules locally, and the @firebase/rules-unit-testing package lets a test act as any user and assert that a request fails. If firebase.json names no rules file and the test loads none, the emulator treats the project as open, so point it at yours.

  4. 4

    Run them on every change

    Cover the three callers that matter: a visitor with no account, the owner and another user. Run the tests before each deploy, and whenever a coding agent touched the rules.

javascript
import { readFileSync } from "node:fs";
import { initializeTestEnvironment, assertFails, assertSucceeds } from "@firebase/rules-unit-testing";

const env = await initializeTestEnvironment({
  projectId: "demo-rules-test",
  firestore: { rules: readFileSync("firestore.rules", "utf8") },
});

const alice = env.authenticatedContext("alice").firestore();
const bob = env.authenticatedContext("bob").firestore();
const visitor = env.unauthenticatedContext().firestore();

await assertSucceeds(alice.doc("users/alice").get());   // the owner
await assertFails(bob.doc("users/alice").get());        // another user
await assertFails(visitor.doc("users/alice").get());    // no account

await env.cleanup();
Save it as rules.test.mjs and run firebase emulators:exec --only firestore "node rules.test.mjs": the CLI starts the emulator and the test finds it on its own. It never touches your live project.

Traps people miss after the first fix

  • A leftover catch-all. Fixing the rules for users changes nothing while a catch-all recursive wildcard further down still lets everyone in. Allows add up.
  • One product fixed, two left open. Firestore, the Realtime Database and Storage each have their own rules. A project that started all three in test mode needs three fixes.
  • A profile that holds its own role. If users can write their whole profile document, every field your rules or your app trust, like role, isAdmin or plan, is theirs to set. Limit writable fields with affectedKeys().hasOnly(), or move roles to custom claims.
  • Email domain checks without verification. request.auth.token.email.matches('.*@yourcompany[.]com') passes for anyone who signs up with that address before verifying it. Add request.auth.token.email_verified == true.
  • App Check taken for access control. App Check helps make sure requests come from your app. Which user may read which document is still up to the rules.

What Keybrum tests on a live Firebase app

Rules on paper show what you meant. An attack shows what a stranger gets. On an app whose owner proved ownership, Keybrum reads the Firebase config from the JavaScript, takes the collection names the code uses (or common ones like users, orders and messages when the code names none), and then:

  • reads each Firestore collection with no account, then again as a brand-new account it signs up itself;
  • tries a write with a probe that can never create a document: rules are checked first, so the answer shows whether the write would have been allowed, and nothing is stored;
  • reads the root of the Realtime Database;
  • lists the Storage bucket and flags the files that look sensitive;
  • looks for API keys and other secrets saved inside the documents it could read.

The Firebase security guide covers the rest of a Firebase app beyond the rules, and the checklist for apps built with AI puts them next to everything else to check before launch.

Questions

What are Firebase test mode rules?

Test mode is the starting option that leaves a new database open to everyone for a while, so you can build before writing rules. In Firestore it is a rule that compares request.time with a fixed date: the Firebase CLI's starter file sets it 30 days ahead, and after that every client request is denied. Replace it with real rules before launch instead of moving the date.

Is allow read, write: if true ever safe in Firestore?

Not for writes: it lets anyone change or delete every document it covers. allow read: if true alone is fine for content you would publish anyway, like a public product list, as long as it is scoped to that collection and not to the whole database.

Is request.auth != null enough in Firestore security rules?

Only for data every signed-in user is meant to share. It checks that the caller has an account, not that the data is theirs, and anyone can create an account, an anonymous one included. Compare request.auth.uid with the document's owner instead.

Is the Firebase API key a secret?

No. Firebase says its API keys only identify your project and are safe to include in your code. That is exactly why the rules matter: anyone can use the key to call your database, and only the rules decide what they get. See Firebase API key exposed.

Do security rules apply to the Firebase Admin SDK?

No. The Admin SDK, and Cloud Functions that use it, reach your data whatever the rules say. That makes server code the right place for privileged actions. It also means a leaked service account key is a far bigger problem than a visible API key, see API keys exposed in the frontend.

What is the difference between Firestore and Realtime Database rules?

Firestore and Storage share a rules language with match and allow, and grant access if any matching rule allows it. The Realtime Database uses JSON with .read, .write and .validate, and a rule on a parent path overrides the rules on its children. Each product needs its own rules.

How can I check my Firebase rules for free?

Paste them into the Firebase rules checker: it runs in your browser and flags open rules, test mode dates and auth-only checks. Then confirm in the Rules Playground of the console, as a visitor with no account and as a second user.

Sources

  1. 1.Avoid insecure rules, Firebase Docs
  2. 2.Fix insecure rules in Cloud Firestore, Firebase Docs
  3. 3.Get started with Cloud Firestore (test mode), Firebase Docs
  4. 4.Firestore starter rules in the Firebase CLI, Firebase on GitHub
  5. 5.Structuring Cloud Firestore Security Rules, Firebase Docs
  6. 6.Writing conditions for Cloud Firestore Security Rules, Firebase Docs
  7. 7.Securely query data (rules are not filters), Firebase Docs
  8. 8.Control access to specific fields, Firebase Docs
  9. 9.Core syntax of Realtime Database Security Rules, Firebase Docs
  10. 10.Use conditions in Realtime Database Security Rules, Firebase Docs
  11. 11.Get started with Firebase Realtime Database (test mode), Firebase Docs
  12. 12.Use conditions in Firebase Security Rules for Cloud Storage, Firebase Docs
  13. 13.Core syntax of Firebase Security Rules for Cloud Storage, Firebase Docs
  14. 14.Control access with custom claims and security rules, Firebase Docs
  15. 15.Build unit tests, Firebase Docs
  16. 16.Firebase security checklist, Firebase
  17. 17.Misconfigured Firebase sites leaked 125 million records, The Register

Your app is public. So are its mistakes.

Check what any attacker can already see in it, free, in ten seconds. The full attack, with proof and fixes: €49 once, or €29 a month on a plan.

or continue with Google or GitHub