
Firebase Security Rules: The Misconfigurations That Exposed 19 Million Secrets
Firebase Security Rules: The Misconfigurations That Exposed 19 Million Secrets
In 2022, researchers scanned over 550,000 Firebase instances and found that nearly 916 websites had completely misconfigured security rules, exposing over 125 million user records including plaintext passwords, billing information, and personal health data. A follow-up study in 2024 found the problem had grown worse: 19 million secrets across Firestore, Realtime Database, and Cloud Storage buckets sat openly accessible to anyone with the project's URL.
Firebase is not insecure. Its security rules system is powerful and granular. The problem is that Firebase ships with test mode rules that allow unrestricted access, and developers leave those rules in place through production.
This is especially common in applications built with AI coding tools. When you prompt Cursor or Bolt to "build an app with Firebase," the generated code uses Firebase's client SDK with the project's public configuration. The AI does not generate security rules because rules live in a separate file (firestore.rules or database.rules.json) that is outside the scope of the generated application code.
The result is a fully functional app with a fully exposed database.
Here are the six most dangerous Firebase security rule misconfigurations, what they allow, and the secure alternatives for each.
Understanding Firebase Security Rules
Before diving into the misconfigurations, you need to understand how Firebase security rules work. Firebase has three products with independent rule systems:
- Firestore - Uses
firestore.ruleswith a match/allow syntax - Realtime Database - Uses
database.rules.jsonwith a JSON-based rule tree - Cloud Storage - Uses
storage.ruleswith the same syntax as Firestore
Rules are evaluated on the server side. They cannot be bypassed from the client. If your rules are correct, your data is secure regardless of what the client code does. That is the good news.
The bad news is that misconfigured rules are equivalent to having no rules at all, and Firebase's test mode defaults are maximally permissive.
Misconfiguration 1: Test Mode Left On
When you create a new Firestore database, Firebase asks you to choose between "production mode" and "test mode." Test mode generates rules that allow unrestricted read and write access for 30 days. Most developers choose test mode to get started quickly.
The Dangerous Rule
// Firestore test mode rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 3, 22);
}
}
}
For Realtime Database:
{
"rules": {
".read": true,
".write": true
}
}
What It Allows
Everything. Any person on the internet can read every document in your database, create new documents, modify existing ones, and delete anything. The Firestore version has a time limit, but developers routinely extend it or remove the timestamp check entirely when it expires and their app "breaks."
An attacker needs only your Firebase project ID (which is embedded in your frontend JavaScript) to access your database:
// Attacker's script - your project ID is in your public JS bundle
import { initializeApp } from 'firebase/app';
import { getFirestore, collection, getDocs } from 'firebase/firestore';
const app = initializeApp({
projectId: 'your-project-id', // Extracted from your frontend code
apiKey: 'your-api-key', // Also public
});
const db = getFirestore(app);
// Read every document in every collection
const users = await getDocs(collection(db, 'users'));
users.forEach(doc => console.log(doc.data()));
// Emails, passwords, payment info - all exposed
The Secure Version
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Deny all access by default
match /{document=**} {
allow read, write: if false;
}
// Then add specific rules for each collection
// (see the following sections)
}
}
Always start with a deny-all rule and explicitly open access only where needed. Never use the {document=**} wildcard with a permissive allow statement.
Misconfiguration 2: Overly Permissive Rules
After test mode expires, developers often write broad rules that technically require authentication but still expose data to every logged-in user.
The Dangerous Rule
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
// "Fixed" - now requires auth
allow read, write: if request.auth != null;
}
}
}
What It Allows
Any authenticated user can read and modify any document in the entire database. Firebase Authentication makes it trivial to create an account (email/password, anonymous auth, etc.). An attacker creates a throwaway account and gains full access to every other user's data.
This is functionally equivalent to test mode for any attacker willing to spend 30 seconds creating an account.
The Secure Version
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users collection - users can only access their own document
match /users/{userId} {
allow read, update: if request.auth != null
&& request.auth.uid == userId;
allow create: if request.auth != null
&& request.auth.uid == userId;
allow delete: if false; // Users cannot delete their own profile
}
// Orders collection - users can only access their own orders
match /orders/{orderId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow update, delete: if false; // Orders are immutable
}
}
}
The key principle: every match statement should target a specific collection, and every allow statement should verify that the authenticated user owns the document they are accessing.
Misconfiguration 3: Missing Data Validation Rules
Firebase security rules can validate the shape and content of incoming data. Most developers do not use this capability, which means users can write arbitrary fields with arbitrary values into documents.
The Dangerous Rule
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
// No validation on what fields can be written
}
What It Allows
A user can add any field to their own document, including fields that should be controlled by the server:
// User escalates their own privileges
await setDoc(doc(db, 'users', currentUser.uid), {
name: 'Regular User',
email: 'user@example.com',
role: 'admin', // Self-assigned admin role
plan: 'enterprise', // Free plan upgrade
credits: 999999, // Unlimited credits
isVerified: true, // Skip verification
}, { merge: true });
If your application checks user.role on the frontend to show admin features, the attacker now has admin access. If it checks user.plan to gate features, they have a free upgrade.
The Secure Version
match /users/{userId} {
allow read: if request.auth.uid == userId;
allow create: if request.auth.uid == userId
&& request.resource.data.keys().hasOnly(['name', 'email', 'avatar'])
&& request.resource.data.name is string
&& request.resource.data.name.size() > 0
&& request.resource.data.name.size() <= 100
&& request.resource.data.email is string
&& request.resource.data.email == request.auth.token.email;
allow update: if request.auth.uid == userId
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name', 'avatar'])
&& request.resource.data.name is string
&& request.resource.data.name.size() > 0
&& request.resource.data.name.size() <= 100;
// Note: 'role', 'plan', 'credits' cannot be modified by the user
// Those fields are set by Cloud Functions (server-side)
}
Key techniques:
keys().hasOnly([...])restricts which fields can be present in a new documentdiff(resource.data).affectedKeys().hasOnly([...])restricts which fields can change in an update- Type checks (
is string,is number,is bool) prevent type confusion - Size limits prevent abuse (e.g., storing massive strings)
Misconfiguration 4: Open Storage Buckets
Cloud Storage for Firebase uses its own security rules. Developers who carefully secure their Firestore or Realtime Database often forget that their storage bucket has a separate (and equally permissive) default rule set.
The Dangerous Rule
// storage.rules - default test mode
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if true;
}
}
}
Or the slightly-less-bad-but-still-terrible authenticated version:
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
What It Allows
Any user (or any authenticated user) can:
- Read every file in your storage bucket, including private user uploads, internal documents, and database backups
- Upload any file of any type and any size, potentially filling your storage quota or hosting malicious content
- Overwrite or delete existing files, including other users' avatars, uploaded documents, or application assets
// Attacker lists and downloads all files
const storageRef = ref(storage, '/');
const result = await listAll(storageRef);
result.items.forEach(async (item) => {
const url = await getDownloadURL(item);
console.log(`${item.fullPath}: ${url}`);
// Download each file
});
The Secure Version
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Deny all by default
match /{allPaths=**} {
allow read, write: if false;
}
// User avatars - users can only manage their own
match /avatars/{userId}/{fileName} {
allow read: if true; // Avatars are public
allow write: if request.auth != null
&& request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024 // 5MB limit
&& request.resource.contentType.matches('image/.*');
// Only allow image uploads
}
// Private user files
match /user-files/{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; // 10MB limit
}
}
}
Always validate:
- Ownership - The user ID in the path matches the authenticated user
- File size - Prevent abuse of your storage quota
- Content type - Prevent uploading executable files or unexpected formats
Misconfiguration 5: Auth State Not Checked Correctly
Firebase security rules have access to the request.auth object, which contains the authenticated user's information. Mistakes in how this object is checked lead to rule bypasses.
The Dangerous Rule
// Checking auth state incorrectly
match /admin/{document=**} {
// This checks if the user has a custom claim 'admin'
// But custom claims can be empty, not just true/false
allow read, write: if request.auth.token.admin;
}
// Dangerous: relying on a document field for admin access
match /admin-panel/{document} {
allow read, write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
}
// Problem: if the user can modify their own /users/{uid} document,
// they can set isAdmin = true (see Misconfiguration 3)
What It Allows
If admin status is stored in a Firestore document that the user can modify (due to missing validation rules), the attacker sets isAdmin: true on their own user document and immediately gains access to admin-only collections.
The Secure Version
Use Firebase custom claims (set server-side) for role-based access, not Firestore document fields:
// Set custom claims server-side (Cloud Function or Admin SDK)
// This CANNOT be modified by the client
const admin = require('firebase-admin');
await admin.auth().setCustomUserClaims(userId, { admin: true });
// Security rules check the custom claim
match /admin/{document=**} {
allow read, write: if request.auth != null
&& request.auth.token.admin == true;
}
For more granular roles:
match /organizations/{orgId}/settings/{document} {
allow read: if request.auth != null
&& request.auth.token.orgRoles[orgId] in ['admin', 'editor'];
allow write: if request.auth != null
&& request.auth.token.orgRoles[orgId] == 'admin';
}
Custom claims are:
- Set only through the Firebase Admin SDK (server-side)
- Included in the user's ID token automatically
- Cannot be modified by the client
- Limited to 1000 bytes total (keep them minimal)
Misconfiguration 6: Firestore Rules Allowing Cross-User Access
Subcollections and reference-based queries in Firestore create complex access patterns that are easy to get wrong. The most common mistake is writing rules for a subcollection that do not verify the parent document's ownership.
The Dangerous Rule
// Users have a subcollection of private notes
match /users/{userId}/notes/{noteId} {
allow read, write: if request.auth != null;
// Missing: && request.auth.uid == userId
}
Or with collection group queries:
// Allows collection group queries across all users' notes
match /{path=**}/notes/{noteId} {
allow read: if request.auth != null;
}
What It Allows
User A can read and write User B's notes by directly referencing the path:
// Attacker reads another user's private notes
const notes = await getDocs(
collection(db, 'users', 'victim-user-id', 'notes')
);
notes.forEach(doc => console.log(doc.data()));
// Or using a collection group query to read ALL users' notes at once
const allNotes = await getDocs(
collectionGroup(db, 'notes')
);
The Secure Version
// Subcollection rules must verify parent ownership
match /users/{userId}/notes/{noteId} {
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(['title', 'content', 'createdAt'])
&& request.resource.data.title is string
&& request.resource.data.content is string;
allow update: if request.auth != null
&& request.auth.uid == userId
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['title', 'content', 'updatedAt']);
allow delete: if request.auth != null
&& request.auth.uid == userId;
}
// If you need collection group queries, restrict them carefully
match /{path=**}/notes/{noteId} {
// Only allow if the parent path starts with the current user's document
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
// Requires each note to have a userId field for verification
}
For collection group queries, add a userId field to every document in the subcollection, and verify it in the rule. This is redundant with the path-based ownership check, but collection group queries match across all parent paths, so you need the field-level check as a backup.
Firebase Security Rules Audit Checklist
Use this checklist before every deployment. Every "no" answer is a potential vulnerability.
General
- Test mode rules have been removed. No
allow read, write: if trueor timestamp-based test rules exist. - No wildcard rules grant broad access. Search for
{document=**}combined withallow readorallow write. These should beif falseor very narrowly scoped. - Every collection has explicit rules. If a collection is not mentioned in your rules, it defaults to deny (safe). Verify this is intentional and not an oversight.
Authentication
- Every rule that requires auth checks
request.auth != null. Do not assume auth is present. - Ownership checks use
request.auth.uidcompared to the document path or auserIdfield. Not a document field the user can modify. - Admin/role checks use custom claims, not Firestore document fields. Custom claims are set server-side and cannot be modified by clients.
Data Validation
- Create rules validate required fields and their types. Use
keys().hasOnly(),is string,is number, etc. - Update rules restrict which fields can be modified. Use
diff().affectedKeys().hasOnly(). - Sensitive fields (role, plan, credits) cannot be set or modified by client writes. These should only be writable by Cloud Functions using the Admin SDK.
- String fields have maximum length limits. Prevent storage abuse.
Storage
- Storage rules are separate from Firestore rules and have been configured. Do not assume Firestore rules apply to Storage.
- File uploads validate content type and file size.
- Storage paths include the user ID, and rules verify ownership.
- No wildcard rules allow unrestricted uploads.
Testing
- Rules have been tested with the Firebase Emulator Suite. Write tests for both allowed and denied access patterns.
- Rules have been tested from the perspective of an unauthenticated user.
- Rules have been tested for cross-user access (User A trying to read User B's data).
Testing Your Rules Locally
Firebase provides an emulator suite that lets you test security rules without deploying. Use it.
# Install Firebase CLI if you haven't
npm install -g firebase-tools
# Initialize the emulator
firebase init emulators
# Start the emulators
firebase emulators:start
Write rule tests using the @firebase/rules-unit-testing library:
import {
initializeTestEnvironment,
assertSucceeds,
assertFails,
} from '@firebase/rules-unit-testing';
import { doc, getDoc, setDoc } from 'firebase/firestore';
const testEnv = await initializeTestEnvironment({
projectId: 'test-project',
firestore: {
rules: fs.readFileSync('firestore.rules', 'utf8'),
},
});
// Test: authenticated user can read their own document
test('user can read own profile', async () => {
const alice = testEnv.authenticatedContext('alice');
const aliceDb = alice.firestore();
await assertSucceeds(
getDoc(doc(aliceDb, 'users', 'alice'))
);
});
// Test: authenticated user CANNOT read another user's document
test('user cannot read other profile', async () => {
const alice = testEnv.authenticatedContext('alice');
const aliceDb = alice.firestore();
await assertFails(
getDoc(doc(aliceDb, 'users', 'bob'))
);
});
// Test: unauthenticated user cannot read any documents
test('unauthenticated user cannot read profiles', async () => {
const unauthed = testEnv.unauthenticatedContext();
const db = unauthed.firestore();
await assertFails(
getDoc(doc(db, 'users', 'alice'))
);
});
// Test: user cannot set admin field
test('user cannot escalate privileges', async () => {
const alice = testEnv.authenticatedContext('alice');
const aliceDb = alice.firestore();
await assertFails(
setDoc(doc(aliceDb, 'users', 'alice'), {
name: 'Alice',
role: 'admin', // Should be rejected by validation rules
})
);
});
Add these tests to your CI pipeline. Every pull request should verify that security rules have not regressed.
Automated External Testing
Rule testing with the Firebase emulator verifies your rules work as intended. But it does not test what an external attacker sees. There is a difference between "my rules are syntactically correct" and "my application is actually secure."
PreBreach tests your Firebase application from the outside. Our AI agents attempt the same attacks described in this article: reading other users' data, writing to collections without proper auth, uploading files to storage, and exploiting misconfigured rules. We test against your deployed application with the same tools and techniques an attacker would use.
The scan covers:
- Open Firestore collections - Attempting to read every collection with unauthenticated and authenticated requests
- Realtime Database exposure - Appending
.jsonto your database URL to check for open read access - Storage bucket enumeration - Listing and downloading files from Cloud Storage
- Cross-user access - Creating two test accounts and attempting to access each other's data
- Write abuse - Attempting to create, modify, and delete documents across collections
- Rule bypass techniques - Testing edge cases in rule evaluation that static analysis misses
Each finding includes the specific rule that needs to change, the exact syntax for the secure version, and a CVSS v4.0 severity score to help you prioritize fixes.
The Cost of Misconfigured Rules
Firebase security rule misconfigurations are not theoretical vulnerabilities. They are actively exploited. Exposed databases lead to:
- Data breaches - User emails, passwords, personal information, and payment data exposed
- Account takeover - Attackers modify user records to gain access to other accounts
- Financial loss - Unauthorized writes to your database can consume your Firebase quota and trigger unexpected billing
- Compliance violations - Exposed personal data violates GDPR, CCPA, HIPAA, and other regulations regardless of whether the exposure was intentional
- Reputation damage - A public breach erodes the trust you have built with your users
The fix is straightforward: write specific rules for every collection, validate all incoming data, use custom claims for roles, secure your storage buckets, and test everything. It takes a few hours to get right, and it prevents the kind of incidents that take months to recover from.
Do not let the convenience of Firebase's defaults become the source of your next security incident.