Guide
Firebase Security Rules Best Practices
In Firebase, the config object with your API key is meant to ship in the frontend: it identifies your project, it does not protect your data. Who can read and write what in Cloud Firestore, the Realtime Database and Cloud Storage is decided by Firebase Security Rules alone. Getting them right means denying by default, checking the owner on every document, validating fields on every write, and testing the result with the emulator and a second account.
- According to the Firebase documentation, the Firebase API key is not a secret. Access to data is controlled by Security Rules for users and App Check for apps, not by the key.
- The most common mistakes: open rules left over from test mode, rules that only check for a signed-in user, no ownership check, no field validation and overly broad Storage rules.
- Server client libraries and the Admin SDK bypass the rules. Their credentials never belong in an app or a repository.
- App Check verifies that a request comes from your genuine app. It does not replace the rules that decide which user may see which data.
How Firebase protects data
Public key, rules, Admin SDK.
Go to sectionCommon mistakes
From test mode to the open bucket.
Go to sectionApps built fast
Why the gap stays invisible.
Go to sectionRule examples
Firestore, Storage, Realtime Database.
Go to sectionTesting
Emulator, unit tests, second account.
Go to sectionChecklist
Everything to check before launch.
Go to sectionHow Firebase protects your data
A Firebase app talks to Cloud Firestore, the Realtime Database and Cloud Storage straight from the browser or mobile app, with no server of your own in between. To do that, it ships a config object containing apiKey, projectId and a few other values. Anyone who opens your app can read them.
That is by design. The Firebase documentation on API keys states that keys for Firebase services only identify your project and app. They do not need to be treated as secrets and are fine to include in code or checked-in config files. The same page is explicit that API keys are not used to control access to backend resources: that is done with Firebase Security Rules for your users and App Check for your apps. The Firebase security checklist says the same. Firebase adds two caveats: restrict every key to the APIs the app actually uses, and use separate keys for other Google services.
The consequence: an attacker does not need a stolen key to query your database. They lift the config from your app and send their own requests. What they can see or change is decided by your rules and nothing else.
Each of the three products has its own rules. Firestore and Cloud Storage share a rules language built on match and allow statements; the Realtime Database uses a JSON structure with .read, .write and .validate. Three principles apply across the board:
- Rules only govern client access. According to the Firestore documentation, the server client libraries bypass all Security Rules and authenticate with Google service accounts instead. The Admin SDK and Cloud Functions can also reach your data regardless of the rules.
- Matching rules are combined with OR. If any matching rule grants access, the request is allowed, even if another matching rule would deny it.
- Rules are not filters. Firestore evaluates a query as a whole. If it could return documents the user is not allowed to read, the documentation says the entire request fails. Your app therefore has to query the way the rule expects, for example by filtering on the current user's ID.
When you create a database or Storage bucket, you choose between locked mode and test mode. In locked mode, Firestore and the Realtime Database deny all access according to the documentation, while Cloud Storage admits any authenticated user. Test mode grants access to everyone.
The common mistakes
None of these are weaknesses in Firebase. They are rules doing exactly what they say. Firebase describes most of them itself on its “Avoid insecure rules” page.
1. Test mode rules never get replaced
Firebase calls test mode good for getting started, but it lets anyone read and overwrite your data. What you end up with is allow read, write: if true; or a rule that only checks a date:
// DO NOT USE: anyone can do anything until this date
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 12, 1);
}
Until that date the database is open to anyone who knows the project ID. Firebase warns that anyone can then steal, modify or delete your data. For the Realtime Database, the documentation says test mode ends after a trial period: you are alerted by email, and then your rules deny all requests. The app suddenly stops working. Pushing the date back at that point simply keeps the database open.
2. Only checking that someone is signed in
A rule like allow read, write: if request.auth != null; looks like protection. Firebase lists it as not recommended: any signed-in user gets read and write access to your entire database. If anyone can sign up for your app, that is effectively public. Anonymous authentication makes it worse. The security checklist points out that anyone can create anonymous accounts, so non-public data should require a non-anonymous sign-in or a verified email address.
3. No ownership check
Every rule protecting personal data needs to compare against the signed-in user's ID, request.auth.uid. There are two patterns: the owner is part of the path, such as /users/{userId}, or stored in a field, such as ownerId. With the second pattern, often only reads are protected. If creates do not check that request.resource.data.ownerId matches the caller, a user can create documents on someone else's behalf. If updates do not check it, they can hand their own document over to another account.
4. No field validation
If a user may write their profile document, then without further checks they may set any field in it, including role, isAdmin or a subscription tier. If your app or your rules later derive permissions from those fields, anyone can promote themselves. Firestore has the tools for this, described in the documentation on field-level control:
request.resource.data.keys().hasOnly([...])restricts which fields a new document may contain.request.resource.data.diff(resource.data).affectedKeys().hasOnly([...])restricts which fields an update may change.- Type checks such as
is string,is intoris timestamp, plus length limits, keep out unexpected values.
Reads are a different story. Firestore always decides reads at the document level, and the documentation states that rules alone cannot prevent users from reading specific fields within a document. Sensitive fields belong in a separate document with its own rule.
5. Rules that behave differently than expected
Three properties of the rules language regularly cause gaps:
- A catch-all wildcard. In version 2 of the rules language, a rule on
match /{document=**}applies to every document in the database. Because matching rules are combined with OR, a broad rule there overrides every narrower rule below it. - Subcollections. In Firestore, a rule applies only at the path it matches, per the documentation. A subcollection such as
/users/{userId}/invoicesneeds rules of its own. - Cascading in the Realtime Database. There,
.readand.writework top-down, and the documentation says shallower rules override deeper ones. A".read": truehigher up cannot be revoked further down..validaterules, by contrast, do not cascade and are ignored when data is deleted.
6. Cloud Storage left too open
The default Cloud Storage rules give every authenticated user access, not just the owner of a file. For invoices, ID documents, contracts or private photos that is not enough. Storage rules need the same ownership check as the database, usually via a path containing the user ID. Uploads also need limits: without checks on size and content type, any user can put arbitrary files of any size into your project, and you pay for them.
7. Admin credentials in the client
Per the documentation, the Admin SDK runs in privileged environments such as a server, with full admin privileges, and bypasses the rules. It authenticates as a service account, often via a JSON file containing a private key. The security checklist calls these keys sensitive and says they must be kept secret. Once such a file ends up in a web app, an app bundle or a public repository, your rules no longer matter. If that happens, replace the key immediately and delete the old one.
8. Treating App Check as a substitute for rules
App Check verifies that a request comes from your genuine app on a genuine device. The App Check documentation describes App Check and Firebase Authentication as complementary: Authentication protects your users, App Check protects you, the developer, from abuse of your backends. Firebase also notes that App Check does not eliminate all abuse. A signed-in user running your genuine app passes App Check. Whether they may read someone else's data is still decided by the rules alone. Firebase recommends enabling App Check for every supported service, as an extra layer, not a replacement.
Why apps built quickly are especially exposed
With AI coding tools, templates or app builders, a working Firebase app can come together very fast. The risk lies less in the generated code than in what gets checked afterwards, because broken rules are invisible in everyday use:
- The builder tests with their own account. They see their own data and everything works, even if they could just as well read everyone else's.
- Errors appear when rules are too strict, not when they are missing. Because rules are not filters, a query without the matching filter fails outright. Feed that permission error back into the tool and you quickly get a fix that makes the app work. Whether it opens access wider than necessary is something someone has to check deliberately.
- Test mode rules work perfectly during development. With the Realtime Database they only surface when the trial period ends and the app suddenly gets no data.
- The Firebase console and server code using the Admin SDK are not subject to the rules. A test there tells you nothing about your rules.
That is why the Firebase security checklist advises against writing rules as a pre-launch task and recommends treating them like a database schema, written alongside the app. If you are on Supabase instead, the equivalent guide is Supabase Row Level Security Best Practices.
Rule examples: minimal and correct
The Firestore example below covers the two most common cases: one profile document per user at /users/{userId}, and notes in /notes whose owner is stored in an ownerId field. Anything not explicitly allowed stays denied.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() {
return request.auth != null;
}
function isUser(uid) {
return signedIn() && request.auth.uid == uid;
}
function validNote(d) {
return d.title is string && d.title.size() <= 200
&& d.body is string && d.body.size() <= 10000;
}
// Profile: the user only, allowed fields only
match /users/{userId} {
allow read: if isUser(userId);
allow create: if isUser(userId)
&& request.resource.data.keys().hasOnly(['displayName'])
&& request.resource.data.displayName is string
&& request.resource.data.displayName.size() <= 80;
allow update: if isUser(userId)
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['displayName'])
&& request.resource.data.displayName is string
&& request.resource.data.displayName.size() <= 80;
allow delete: if false;
}
// Notes: owner stored in the document
match /notes/{noteId} {
allow read, delete: if isUser(resource.data.ownerId);
allow create: if isUser(request.resource.data.ownerId)
&& request.resource.data.keys().hasOnly(['ownerId', 'title', 'body'])
&& validNote(request.resource.data);
allow update: if isUser(resource.data.ownerId)
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['title', 'body'])
&& validNote(request.resource.data);
}
}
}
Four details matter here. rules_version = '2'; must be the first line. There is no rule on {document=**}, so collections not listed here are not accessible at all. Users cannot set fields like role or isAdmin, because hasOnly excludes them; create such values on the server. And ownerId cannot change on update, because it is not in the allowed list.
Since rules are not filters, the app has to query its notes so that the rule permits every possible result, that is, filtered on ownerId equal to the current user's ID. A query over the whole collection fails, and that is the intended behaviour.
The same principle applies to Cloud Storage. This example lets each user access only files in their own folder and, on upload, accepts only images under 5 MB. The limits follow the example in the Storage documentation:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{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/.*');
}
}
}
If you still use the Realtime Database, the same logic looks like this. $uid is the wildcard for the user ID in the path, and "$other" rejects any field not listed:
{
"rules": {
"notes": {
"$uid": {
".read": "auth !== null && auth.uid === $uid",
".write": "auth !== null && auth.uid === $uid",
"$noteId": {
".validate": "newData.hasChildren(['title', 'body'])",
"title": { ".validate": "newData.isString() && newData.val().length <= 200" },
"body": { ".validate": "newData.isString() && newData.val().length <= 10000" },
"$other": { ".validate": false }
}
}
}
}
}
One caveat: no ".read": true may sit higher up, for example directly under "rules" or "notes". Because of cascading, it would apply to everything below.
How to test your rules
Only test projects you own or have written permission to test. Firebase ships its own tooling for this.
Step 1: The Rules Playground for a quick check
The rules editor in the Firebase console offers the Rules Playground for Firestore, the Realtime Database and Storage. You simulate a read or write on a path, unauthenticated, as an anonymous user or as a specific user ID, and see whether the rules allow it. Firebase itself positions the playground for quick checks and points to the emulator for full testing.
Step 2: Rules unit tests against the emulator
The Firebase Local Emulator Suite runs Firestore, the Realtime Database and Storage locally, for example with firebase emulators:start --only firestore. For automated tests there is the @firebase/rules-unit-testing library. It simulates signed-in and signed-out users, and assertSucceeds and assertFails check whether an operation is allowed or rejected. A test for the notes rules above, following the documented API:
import { readFileSync } from 'node:fs';
import {
initializeTestEnvironment, assertSucceeds, assertFails,
} from '@firebase/rules-unit-testing';
const env = await initializeTestEnvironment({
projectId: 'demo-rules-test',
firestore: { rules: readFileSync('firestore.rules', 'utf8') },
});
// seed test data with rules disabled
await env.withSecurityRulesDisabled(async (ctx) => {
await ctx.firestore().doc('notes/n1')
.set({ ownerId: 'alice', title: 'A', body: '' });
});
const alice = env.authenticatedContext('alice').firestore();
const bob = env.authenticatedContext('bob').firestore();
const guest = env.unauthenticatedContext().firestore();
await assertSucceeds(alice.doc('notes/n1').get());
await assertFails(bob.doc('notes/n1').get()); // someone else's note
await assertFails(guest.doc('notes/n1').get()); // signed out
await assertFails(bob.doc('notes/n2') // on alice's behalf
.set({ ownerId: 'alice', title: 'x', body: '' }));
await assertFails(alice.doc('notes/n1') // change the owner
.update({ ownerId: 'bob' }));
await env.cleanup();
For continuous integration, Firebase recommends firebase emulators:exec: it starts the emulators, runs your test script, for example firebase emulators:exec --only firestore "npm test", and shuts them down afterwards. The security checklist advises adding these tests to your CI pipeline, so a later change that loosens a rule gets caught.
Step 3: Test with a second account
The final test runs against your real project. Create two test accounts, A and B, and use A to create some records and upload a file. Then sign in as B and, from your app or a small test script using the Firebase client, try to:
- read one of A's documents directly by its ID,
- query the whole collection without a filter,
- create a document with A's
ownerId, and update or delete one of A's documents, - add an extra field such as
roleto your own profile, - download a file from A's folder and upload a large or unexpected file type.
Every one of these attempts must fail with a permission error. Repeat the tests while signed out. Keep in mind that, per the documentation, changes to Firestore rules can take up to a minute to affect new queries and up to ten minutes to reach active listeners, so do not test the instant you deploy.
Pre-launch checklist
| Area | Action |
|---|---|
| Test mode | No rule with if true or a date-only condition left active in Firestore, the Realtime Database or Storage |
| Default | Everything denied unless explicitly allowed; no broad rule on {document=**} |
| Ownership | Every rule for personal data compares against request.auth.uid, including on create and update |
| Fields | Allowed fields via hasOnly, updates via affectedKeys(), types and lengths checked; roles and tiers not user-editable |
| Sensitive fields | Stored in separate documents with their own rule, since reads are controlled per document |
| Subcollections | Every subcollection has its own rule |
| Realtime Database | No high-level .read or .write that overrides deeper rules |
| Storage | Ownership check per path, upload size and content type limited |
| Keys | Only the Firebase config in the client, API key restricted; service account keys on the server only |
| Sign-in | Anonymous accounts get no access to non-public data |
| App Check | Enabled for every supported service, in addition to the rules |
| Tests | Rules unit tests in the emulator run in CI; second-account and signed-out tests passed |
What testing your own rules will not catch
Unit tests cover exactly the cases you thought of. Whether a Cloud Function using the Admin SDK skips a check, whether a team member can see another team's invoices, or whether a chain of individually allowed steps still reaches someone else's data often only shows up when someone deliberately attacks the app with several roles. If personal data is actually accessed in the process, that can trigger a notification obligation under Article 33 GDPR. Whether it does in a given case is a legal question. This article is not legal advice.
Would your Firebase rules hold up against a real attack?
We test apps built on Firebase, including those built with AI tools, within a scope agreed in writing: rules for Firestore, the Realtime Database and Storage, write permissions and fields that carry privileges, keys in the client, sign-in and Cloud Functions, each with several test accounts and without a session. Every finding comes with reproducible evidence and a concrete fix. Learn more about the AI app security check.
Request an app security checkCyberSec42 is an independent technical security assessment, not an accredited certification body, and does not provide legal advice. CyberSec42 is not affiliated with Google or Firebase.
Frequently asked questions
Is it safe to expose the Firebase API key in the frontend?
Yes. According to the Firebase documentation, keys for Firebase services only identify your project and app and do not need to be kept secret. Restrict the key to the APIs you use. Protecting your data is the job of Security Rules.
Is request.auth != null enough?
No, not for per-user data. It lets every signed-in user read and write all of your data, and Firebase lists it as not recommended. Also compare request.auth.uid with the owner of the document or file.
What happens when test mode rules expire?
For the Realtime Database, the documentation says you are alerted by email and then the rules deny all requests. Replace them with real access rules at that point rather than moving the expiry date. A later date keeps the database open to everyone.
Does App Check replace Security Rules?
No. App Check verifies that a request comes from your genuine app. Which user may read or change which data is still governed by the rules. Firebase recommends App Check as an additional layer.
Do the rules apply to the Admin SDK and Cloud Functions?
No. Server client libraries and the Admin SDK bypass Security Rules. Checks in Cloud Functions have to be written in your own code, and service account keys belong on the server only.
How do I test Firebase Security Rules automatically?
With the Firebase Local Emulator Suite and the @firebase/rules-unit-testing library. It simulates signed-in and signed-out users, and firebase emulators:exec runs the tests in CI.
Further reading
- Vibe coding security checklist
- Supabase Row Level Security Best Practices
- Lovable Security Issues, Scan and Checklist
- API Pentesting Checklist: OWASP API Top 10
- Service: AI app security check
Sources
- Firebase Docs: Learn about using and managing API keys for Firebase
- Firebase: Security checklist
- Firebase Docs: Avoid insecure rules
- Firebase Docs: Basic Security Rules
- Firebase Docs: Get started with Cloud Firestore Security Rules
- Firebase Docs: Structuring Cloud Firestore Security Rules
- Firebase Docs: Securely query data
- Firebase Docs: Control access to specific fields
- Firebase Docs: Realtime Database Security Rules language
- Firebase Docs: Realtime Database rules conditions
- Firebase Docs: Realtime Database installation and setup (Web)
- Firebase Docs: Understand Firebase Security Rules for Cloud Storage
- Firebase Docs: Add the Firebase Admin SDK to your server
- Firebase Docs: App Check
- Firebase Docs: Rules Playground
- Firebase Docs: Build unit tests
- Firebase Docs: Install, configure and integrate Local Emulator Suite
- EUR-Lex: General Data Protection Regulation (EU) 2016/679