Firestore and Realtime Database are not protected by your application. They are protected by rules that live in the database and are evaluated on every request, because the client talks to the database directly. Get the rules wrong and there is no server-side code to save you — the security boundary is the rule file, and nothing else.
The most common way to get it wrong is not subtle. It is allow read, write: if true;, left in place from the day the project was created.
How the Default Becomes Production
When you create a Firestore database, the console offers test mode and production mode. Test mode writes a rule that allows all reads and writes for thirty days, and it exists for a good reason: it lets you build without fighting permissions while nothing real is in the database.
Then the thirty days lapse, the app breaks, and the fastest fix — the one every tutorial and Stack Overflow answer suggests — is to change the timestamp condition to if true. Development continues. The app ships. The rule is still there, and now it guards real customer data.
The pattern concentrates in a specific kind of project: mobile and web MVPs built quickly, often with Flutter or React Native, frequently by teams whose strength is product rather than backend. Firebase is popular precisely because it removes the backend, and removing the backend also removes the place where authorisation normally lives. That trade is fine as long as the rules take up the job, and the rules are the first thing to be postponed.
What "Wide Open" Actually Exposes
Your Firebase project ID is not a secret — it is in your client bundle and in your mobile app, because the client needs it to connect. With the ID and an open rule, anyone can query the database over the public REST and SDK endpoints without touching your application at all.
Read access means the entire collection tree: user documents, order history, chat messages, uploaded file references, any admin collection you created for internal tooling. Write access means creating and modifying documents — changing a subscription field, editing a price, inserting content that your app will render to other users, or deleting collections outright.
Two consequences catch teams by surprise. First, your application's own validation is irrelevant, because requests never pass through it: a field your UI would never send is accepted if the rule permits the write. Second, your billing is exposed — Firestore charges per document read, so an open database is not only a data breach but a cost incident, and a script iterating your collections can run a bill up fast.
Storage rules deserve the same look. The same permissive pattern in storage.rules makes every uploaded file readable, and lets an attacker replace a file with one of their own choosing.
The Failure Modes That Look Secure and Are Not
Beyond if true, three rule patterns pass a glance and fail a test.
Authenticated is not authorised. allow read, write: if request.auth != null; is the second most common rule we see, and it means any signed-in user can read and write everything — including other users' documents. If your app allows self-service signup, this is functionally open to the internet with one extra step.
Ownership checked on read, not on write. Rules are often written carefully for read and loosely for write, because reads are what the developer was debugging. The result is a database where you cannot see someone else's document but you can overwrite it.
Validation missing on shape. Even a correct ownership rule permits a user to write arbitrary fields into their own document — including role: "admin" or plan: "enterprise", if your application trusts those fields. Rules can constrain the shape of a write with request.resource.data, and most rule files never do.
How to Check Your Own Project
Read the rules first, in the Firebase console under Firestore or Realtime Database, and again under Storage. If you see if true, or a bare request.auth != null on a collection holding user data, you have your answer without testing anything.
Then test empirically, because the rules you deployed are not always the rules you have in your repository. Create two ordinary user accounts, and from account A attempt to read and write a document belonging to account B:
# Firestore REST, as an authenticated user
curl -H "Authorization: Bearer <id-token-for-user-A>" \
"https://firestore.googleapis.com/v1/projects/<project-id>/databases/(default)/documents/users/<user-B-doc-id>"
Any response other than a permission error is a finding. Repeat without a token at all, which tests the unauthenticated path, and repeat for writes rather than assuming they mirror reads. The Firebase emulator suite runs the same rules locally, so these checks belong in your test suite rather than in a periodic audit.
The Fix
Write rules that assert ownership and shape, per collection, and deny by default. The general form is unglamorous and effective:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if request.auth != null && request.auth.uid == userId;
allow update: if request.auth != null && request.auth.uid == userId
&& !request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['role', 'plan', 'credits']);
}
match /{document=**} {
allow read, write: if false;
}
}
}
Three things are doing the work there: ownership is checked against the authenticated UID rather than against a field in the document, privileged fields are explicitly excluded from what a user may change, and the catch-all at the end denies anything you forgot to write a rule for. That last clause is the one that converts an incomplete rule file from dangerous to merely incomplete.
Then move rules into CI. They are a text file in your repository, the emulator can evaluate them, and a test that asserts "user A cannot read user B's document" is a few lines. Rules that are only reviewed when something breaks will drift, because every new collection is a new opportunity to forget.
Why This Keeps Happening
Because the platform's greatest strength is also where it puts the sharpest edge. Firebase lets a small team ship a real product without a backend, and in exchange it asks them to implement authorisation in a declarative language they will use once and then not touch for a year. The permissive rule is not laziness; it is the only state in which the tutorial works.
It also fails silently in the direction of working. An over-permissive rule never produces an error, never breaks a build, and never appears in a bug report — the app functions perfectly, for everyone, including people who are not your users. That combination is why this class survives code review and lands in automated testing instead: a scan can hold two accounts and try the thing your rules were supposed to prevent, which is the only way to know they do.
