Firestore Rules allow read, write: if true? Replace It Safely
If your Firestore rules allow read, write to everyone, with allow read, write: if true; or the test-mode rule that compares request.time to a date, anyone who has your project ID can read, change and delete every document in the database. You don't fix it by moving the date forward. Replace the open rule with one block per collection that checks request.auth.uid against the owner of the data, validate what clients write, test it in the emulator, and deploy it from a rules file you keep in git.
What "allow read, write: if true" means in Firestore rules
This is the rule, usually generated by a tutorial, an AI coding tool or a project template:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}match /{document=**} matches every document in the database, at any depth. allow read, write: if true grants every operation: get, list, create, update and delete. Firebase's own guide to insecure rules shows this exact rule set under the comment "NEVER use this rule set in production" and puts the risk plainly: "anyone who guesses your project ID can steal, modify, or delete the data."
Nobody has to guess. The project ID ships to every browser in your web config, next to the apiKey, and that key is public by design: your data is protected by Security Rules, not by hiding the key (how to hide an API key in the frontend explains why). Anyone can paste your config into their own script and query your collections directly, without ever touching your app's screens or login page.
Firestore test mode rules: the 30-day timer
The other common version looks safer because it has an end date. firebase init writes it from this template in the Firebase CLI, with the date filled in 30 days ahead:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 11, 28);
}
}
}Until that date, the whole database is open. After it, every client request is denied and the app stops working. The template's comment says the rule "is configured to expire after 30 days because it leaves your app open to attackers," and Firebase describes console test mode the same way: it "allows anyone to read and overwrite your data."
If Firebase alerted you that your Cloud Firestore database isn't properly secured, look for this rule or the if true one first. Pushing the date out a year leaves the database open for a year. Replace the rule instead.
Firebase rules for authenticated users are not enough
The usual first fix:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}It only checks that someone is signed in. If your app has a sign-up form, anyone can create an account and then read and write every document the rule covers, other users' data included. With anonymous sign-in enabled, they don't even need an email address. Firebase's guide files this under "Not recommended: Any logged-in user has read and write access to your entire database."
request.auth != null is a building block, not a rule. Every condition on user data also has to compare request.auth.uid with the document path or an owner field.
Firestore security rules examples to replace it
Allow only what the app does. Per Firebase's structure guide, you can only access documents your rules specifically allow, so a collection with no match block is closed to clients. Here is a complete file for an app with private user data and public posts:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isOwner(userId) {
return request.auth != null && request.auth.uid == userId;
}
// Private data: users/{uid} and everything under it
match /users/{userId}/{document=**} {
allow read, write: if isOwner(userId);
}
// Public read, owner write
match /posts/{postId} {
allow read: if resource.data.published == true
|| isOwner(resource.data.authorId);
allow create: if isOwner(request.resource.data.authorId)
&& validPost(request.resource.data);
allow update: if isOwner(resource.data.authorId)
&& request.resource.data.authorId == resource.data.authorId
&& validPost(request.resource.data);
allow delete: if isOwner(resource.data.authorId);
}
function validPost(data) {
return data.keys().hasAll(['title', 'body', 'authorId', 'published'])
&& data.keys().hasOnly(['title', 'body', 'authorId', 'published'])
&& data.title is string && data.title.size() > 0 && data.title.size() <= 200
&& data.body is string && data.body.size() <= 20000
&& data.published is bool;
}
}
}This file was run against the Firestore emulator with the tests further down: visitors and other users are denied, owners are allowed, and writes with extra fields or a changed authorId are rejected.
Owner-only documents
Keep each user's private data under a path that contains their user ID, such as users/{uid} and users/{uid}/notes/{noteId}, and compare that path segment with the signed-in user's ID. With rules_version = '2', a recursive wildcard matches zero or more path segments, so /users/{userId}/{document=**} covers the user document and everything below it. That's a safe use of {document=**}, because it starts inside a path the user owns.
Public read, owner write
For data anyone may see, keep the owner's ID in a field and check it on every write, as in the "mixed public and private access" pattern in Firebase's rules basics:
resource.datais the document as it is stored now.request.resource.datais the document as it would be after the write.createchecks the incomingauthorId, so nobody can create a post in someone else's name.updatechecks the storedauthorId, and also requires that it doesn't change, so nobody can take over someone else's post.
Rules are not filters: a query that could return a document the rules deny fails as a whole. Query with where('published', '==', true) or where('authorId', '==', uid), not for the whole collection.
Validate fields on write
Firestore has no schema, so unchecked, a client can write isAdmin: true, a 50,000-character title, or a number where you expected a string. validPost uses checks from Firebase's field-level rules guide: hasAll requires fields, hasOnly rejects any field not on the list, and is checks types. To make some fields read-only on update, use request.resource.data.diff(resource.data).affectedKeys().hasOnly([...]) with the fields a client may change.
Rules can't hide fields from reads: a read returns the whole document or nothing, so private fields belong in a separate document.
The {document=**} trap: rules are OR'd
This mistake keeps a database open after you think you've fixed it. When several allow statements match a request, Firestore allows it if any of them is true. So this file is still fully open:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Left over from test mode
match /{document=**} {
allow read, write: if request.time < timestamp.date(2099, 1, 1);
}
}
}The careful users rule does nothing, because the leftover block allows every request anyway. Delete the test-mode block when you write real rules.
The reverse, a catch-all match /{document=**} { allow read, write: if false; } at the end, is harmless but does less than people think. Unmatched paths are already denied, and it can't take back access granted elsewhere, since false OR'd with anything is that thing. Rules also don't cascade in Firestore. A rule on posts/{postId} says nothing about a posts/{postId}/comments subcollection, which needs its own match.
Firebase storage rules and Realtime Database rules
Cloud Storage rules live in storage.rules. The open version is allow read, write; (or allow read, write: if true;) on match /{allPaths=**}, and Firebase's list of insecure rules warns that it makes every file in your bucket accessible to any user. An owner-only version that also limits uploads to images under 5 MB:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Files look like users/<uid>/avatar.png
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/.*');
}
}
}Delete gets its own line: there's no request.resource on a delete, so one write rule that checks size and type would deny it.
Realtime Database rules are JSON, usually in database.rules.json. The open version is ".read": true, ".write": true at the root. One difference matters: Realtime Database rules cascade. A .read or .write granted at a parent covers everything below it, and a rule at a deeper path can't take it back. Owner-only data looks like this:
{
"rules": {
"users": {
"$uid": {
".read": "auth !== null && auth.uid === $uid",
".write": "auth !== null && auth.uid === $uid"
}
}
}
}How to test Firestore rules
Test before you deploy. For a quick check, open your database's Rules tab in the Firebase console and click Rules Playground. Pick a simulation type such as get, enter a path such as /users/alice, choose signed out, anonymous or a specific user ID, and click Run. A banner says whether the request was allowed or denied.
For tests you can rerun on every change, use the Firestore emulator with @firebase/rules-unit-testing. The emulator is part of the Firebase CLI and needs Node.js and a Java JDK (install notes).
npm install -D firebase-tools firebase @firebase/rules-unit-testingSave this as rules.test.mjs next to firestore.rules:
import { readFileSync } from 'node:fs';
import { after, before, beforeEach, test } from 'node:test';
import {
assertFails,
assertSucceeds,
initializeTestEnvironment,
} from '@firebase/rules-unit-testing';
import { doc, getDoc, setDoc } from 'firebase/firestore';
let env;
before(async () => {
env = await initializeTestEnvironment({
projectId: 'demo-rules-test',
firestore: { rules: readFileSync('firestore.rules', 'utf8') },
});
});
beforeEach(async () => {
await env.clearFirestore();
await env.withSecurityRulesDisabled(async (ctx) => {
await setDoc(doc(ctx.firestore(), 'users/alice'), { name: 'Alice' });
});
});
after(async () => {
await env.cleanup();
});
test('a signed-out visitor cannot read a user document', async () => {
const db = env.unauthenticatedContext().firestore();
await assertFails(getDoc(doc(db, 'users/alice')));
});
test('another signed-in user cannot read it either', async () => {
const db = env.authenticatedContext('bob').firestore();
await assertFails(getDoc(doc(db, 'users/alice')));
});
test('the owner can read and write their own document', async () => {
const db = env.authenticatedContext('alice').firestore();
await assertSucceeds(getDoc(doc(db, 'users/alice')));
await assertSucceeds(setDoc(doc(db, 'users/alice'), { name: 'Alice B.' }));
});Start the emulator, run the tests and shut it down in one command:
npx firebase emulators:exec --only firestore --project demo-rules-test "node --test rules.test.mjs"The "another signed-in user" test matters most: it catches the request.auth != null mistake, which a signed-out test never will. The testing library never touches your production project.
How to deploy Firestore rules
Point firebase.json at your rules files:
{
"firestore": { "rules": "firestore.rules" },
"storage": { "rules": "storage.rules" },
"database": { "rules": "database.rules.json" }
}Deploy only the rules, per Firebase's manage and deploy guide:
firebase deploy --only firestore:rules
firebase deploy --only storage
firebase deploy --only databaseA release takes several minutes to fully propagate. With more than one Firestore database, rules are deployed per database. Locking clients out doesn't lock out your server: the Admin SDK and Cloud Functions bypass these rules.
Should you commit Firebase rules to git?
Yes, and deploy them only from the file. Rules can also be edited and published in the console, and that's how the two copies drift apart. Firebase warns that a CLI deploy overwrites the rules in the console, so a console-only fix disappears on the next firebase deploy, and until then the repository says one thing while production runs another.
The console always shows the most recently deployed rules. Before your first CLI deploy, copy anything that exists only there into your file. After that, change rules only in the file.
App Check is a good second layer once the rules are right. It rejects requests that don't come from your real app, but Firebase says it "prevents some, but not all, abuse vectors." It doesn't replace rules.
Checklist before you ship
- No
if true, bareallow read, write;orrequest.time < timestamp.date(...)onmatch /{document=**}, in Firestore or Storage rules. - No
".read": trueor".write": trueat the root ofdatabase.rules.json. - Every rule on user data compares
request.auth.uidwith the path or an owner field.request.auth != nullalone guards nothing. - Creates and updates check the owner field and can't change it.
- Writes are validated with
hasOnly,hasAlland type checks. - Every subcollection the app uses has a
matchthat covers it. - Emulator tests cover a signed-out visitor, the owner and a second signed-up user.
- The deployed rules match the files in git.
The vibe coding security checklist covers the rest of what to check before launch.
Checking your committed rules with Tuvio
Tuvio's free scan needs no account and reads the Firebase rules files committed to your repository on every scan, web or mobile: Firestore and Cloud Storage *.rules files and Realtime Database database.rules.json. It flags rules anyone can write, rules anyone can read, test-mode rules, rules any signed-in user passes, and public reads. A collection that is public on purpose is flagged too, so read each finding against what you meant.
It reads the files in the repository. It doesn't connect to your Firebase project, so rules changed only in the console and never committed aren't seen, which is one more reason to keep your deployed rules in git. The example report shows what a scan returns.