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 trueand the test mode date rule both open everything they cover. The date protects nothing before it passes, and breaks the app after.request.auth != nullmeans any account, and anyone can make one, anonymous ones included. Comparerequest.auth.uidwith the owner saved inresource.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 rules examples that leak data
- Firebase rules examples that hold
- Realtime Database rules: JSON, and parents win
- Cloud Storage rules: owners, sizes and file types
- How to test Firebase security rules before you deploy
- Traps people miss after the first fix
- What Keybrum tests on a live Firebase app
- Questions
- Sources
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.
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;
}
}
}| Part | What 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, write | The operations granted. read splits into get (one document) and list (queries). write splits into create, update and delete. |
request | The 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. |
resource | The 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.
| Rule | Who gets in | What a stranger can do |
|---|---|---|
allow read, write: if true; | Anyone, no account needed | Read, change and delete every document the block covers |
if request.time < timestamp.date(...) (test mode) | Anyone, until the date | The same, then the app breaks when the date passes |
if request.auth != null; | Anyone with an account, anonymous ones included | Sign up, then read or write every user's data |
request.auth.uid == request.resource.data.ownerId on update | Any signed-in user | Take over another user's document by writing their own uid into it |
A catch-all match /{document=**}, or {allPaths=**} in Storage | Whoever the condition lets in, on every path | Reach every collection or file, whatever the narrower rules say |
request.auth.token.email.matches('.*@company[.]com') | Anyone who signs up with that address, unverified | Pass as staff without owning the mailbox |
Open to everyone: allow read, write: if true
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
// 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
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
// 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
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;
}
}
}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.
// On your server, with the Admin SDK
import { getAuth } from "firebase-admin/auth";
await getAuth().setCustomUserClaims(uid, { admin: true });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():
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:
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:
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.
{
"rules": {
".read": true,
"users": {
".read": false
}
}
}{
"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 }
}
}
}
}$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:
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
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(ordatabase, orstorage) from an old file overwrites a fix made in the console. - 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
Write unit tests against the emulator
The Firebase emulator runs your rules locally, and the
@firebase/rules-unit-testingpackage lets a test act as any user and assert that a request fails. Iffirebase.jsonnames no rules file and the test loads none, the emulator treats the project as open, so point it at yours. - 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.
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();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
userschanges 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,isAdminorplan, is theirs to set. Limit writable fields withaffectedKeys().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. Addrequest.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.Avoid insecure rules, Firebase Docs
- 2.Fix insecure rules in Cloud Firestore, Firebase Docs
- 3.Get started with Cloud Firestore (test mode), Firebase Docs
- 4.Firestore starter rules in the Firebase CLI, Firebase on GitHub
- 5.Structuring Cloud Firestore Security Rules, Firebase Docs
- 6.Writing conditions for Cloud Firestore Security Rules, Firebase Docs
- 7.Securely query data (rules are not filters), Firebase Docs
- 8.Control access to specific fields, Firebase Docs
- 9.Core syntax of Realtime Database Security Rules, Firebase Docs
- 10.Use conditions in Realtime Database Security Rules, Firebase Docs
- 11.Get started with Firebase Realtime Database (test mode), Firebase Docs
- 12.Use conditions in Firebase Security Rules for Cloud Storage, Firebase Docs
- 13.Core syntax of Firebase Security Rules for Cloud Storage, Firebase Docs
- 14.Control access with custom claims and security rules, Firebase Docs
- 15.Build unit tests, Firebase Docs
- 16.Firebase security checklist, Firebase
- 17.Misconfigured Firebase sites leaked 125 million records, The Register