Test-mode rules, the ones Firebase suggests when you create a new project, read allow read, write: if true. That line lets anyone with your project's URL read or write every document in your database. The fix is auth-scoped rules per collection, locked Storage rules, and a validation step baked into the rule itself. This checklist walks each one with copy-paste rules and an emulator test to confirm before you ship.
How Firebase rules leak data (test mode)
New Firestore and Storage instances start in "test mode": permissive rules meant for local development, with an expiry date after which reads and writes stop entirely. Two things go wrong from here. Either the expiry passes and the app breaks in production (annoying but safe), or someone extends the expiry, or copies the permissive rule into "production mode" rules, to keep shipping without touching the rule file again (works today, leaks tomorrow).
Firebase rules are the only access control Firestore and Storage have. There is no separate database-level permission system behind them the way Postgres has roles and grants. If the rule says yes, the request succeeds, full stop.
Spotting "allow read, write: if true"
This is the line to grep for across every rules file in your project:
match /{document=**} {
allow read, write: if true;
}
A wildcard match plus an unconditional if true opens everything beneath it. It is easy to miss because it often sits above narrower, safer-looking rules further down the file, and broad rules combine with narrow ones by union: if any matching rule allows the request, the request is allowed. A single leftover wildcard defeats every careful rule you wrote elsewhere.
Auth-scoped Firestore rules (examples)
The replacement pattern checks request.auth before granting anything:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /todos/{todoId} {
allow read, write: if request.auth != null;
}
}
}
request.auth != null is the minimum bar: it blocks anonymous requests but still lets any signed-in user touch any document. That is enough for single-tenant admin tools, not enough for user data.
Per-document owner rules
For user-owned data, scope the rule to the document's owner field:
match /todos/{todoId} {
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;
}
Note the split: resource.data is the document as it already exists in the database (used for read, update, delete), while request.resource.data is the document as the client is trying to write it (used for create, and for validating updates). Mixing these up is a common source of rules that look correct and are not.
Lock Storage rules
Storage rules are a separate file from Firestore rules and are just as often left in test mode. The same pattern applies:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{allPaths=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
Anything outside a matched, authenticated path in this file is unreachable by URL, which is what you want for user uploads, private documents, and anything not meant to be a public asset.
Validate data shape in rules
Auth checks answer "who," not "what." A signed-in user can still write a document with the wrong shape, an oversized field, or a value from a range you never intended, unless the rule validates it:
allow update: if request.auth.uid == resource.data.ownerId
&& request.resource.data.title is string
&& request.resource.data.title.size() < 200;
This is the Firestore-rules equivalent of a server-side input check. Skipping it means your client-side validation is the only validation, and a client is something anyone can bypass.
Test rules with the emulator
Firebase ships a local emulator suite specifically so you can test rules without touching production data. Run it, write a small test file with two fake users, and assert that user A cannot read user B's documents and that an unauthenticated request is rejected outright. This catches the mistake where a rule works in the console's rules playground but fails once real client SDKs and real auth tokens are involved.
Verify from outside
Once rules are deployed, confirm from outside your app, the same way an attacker would: an unauthenticated curl against your Firestore REST endpoint, and against a Storage file URL you expect to be private, should both fail. Config review tells you what you intended to allow. Only an outside request tells you what the rule actually allows.