Why the rules file is the whole security model
In a Firebase app there is usually no server between the client and the data. The web or mobile SDK reads and writes Firestore, Realtime Database and Cloud Storage directly using a project configuration that ships inside your JavaScript bundle. Firebase's own documentation puts it plainly: Firebase allows clients direct access to your data, and Firebase Security Rules are the only safeguard blocking access for malicious users.
That is the thing AI coding tools most often get wrong, because it is invisible in the preview. The generated app renders, the signed-in test user sees their own documents, and nothing on screen reveals that the rule allowing that read also allows any visitor with the project ID to read every document in the collection.
The same documentation warns that if you deploy your app it is publicly accessible even if you have not launched it. A staging deploy, a Vercel preview or a Firebase Hosting URL you sent to two friends is enough. If the rules are open, the data is open.
Mistake 1: test mode left on
When you create a Firestore database or a Storage bucket in the console you pick a starting mode. Firebase describes test mode as good for getting started with the mobile and web client libraries, but allows anyone to read and overwrite your data. Production mode, sometimes called locked mode, denies all reads and writes from mobile and web clients while server-side Admin SDK code still works.
The test-mode ruleset the console writes includes a time condition of the form request.time < timestamp.date(...) with a date shortly after the project was created. That line is the only thing standing between your data and the public, and it is a timer, not a lock. AI tools have a bad habit with it: when the date passes and every client call starts failing, the tool's fix is to move the date out or delete the condition, which quietly makes the database open forever.
Search your rules for timestamp.date. If it is there, you are in test mode whether or not you meant to be. Replace the whole match block with rules that name real collections and check real ownership, as in the snippet below.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
match /{document=**} {
allow read, write: if false;
}
}
}Mistake 2: allow read, write: if true
Firebase's insecure-rules guide calls this open access and says that anyone with your project ID can steal, modify or delete your data without authentication. It shows up in three spellings: allow read, write: if true in Firestore and Storage, allow read, write with no condition at all in Storage, and ".read": true with ".write": true in Realtime Database.
AI tools produce it in two ways. The first is a permission-denied error during development, which the tool fixes by widening the rule until the error goes away. The second is a request like "make the leaderboard public", where the tool opens the whole database instead of the one collection you asked about.
The fix is never to hide the project configuration. Firebase's config is meant to be public; it identifies your project, it does not grant privileges. The fix is a rule that requires authentication and then narrows to the specific documents that user may touch.
Mistake 3: signed in is not the same as allowed
The most common rule in generated Firebase projects is allow read, write: if request.auth != null. It looks like security because it mentions auth. Firebase lists it under insecure rules as access for any authenticated user and explains that while it requires login, it grants access to all authenticated users without further restriction on specific data.
In practice that means any person who creates an account, including an attacker who signs up with a throwaway email or anonymous auth, can read and edit every other user's documents. The rule answers "is this a user?" when the question is "is this the user who owns this document?". Firebase's fix is to narrow access using security conditions that compare request.auth.uid against the document's path or a field in the document.
Realtime Database has the same shape with ".read": "auth.uid !== null". The fix uses a wildcard for the user key and compares it: "$uid === auth.uid". Realtime Database rules cascade from parent to child, and a permissive rule at a shallow path overrides anything stricter below it, so put the auth check at the user node, not at the root.
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}Mistake 5: Storage rules that make uploads public
Cloud Storage has its own rules file and it is the one AI tools most often leave alone, because file uploads usually work on the first try. The default rules restrict reads and writes to authenticated users, but a generated project that needs to display avatars or attachments will often get allow read: if true on the whole bucket, sometimes with a comment saying it is temporary.
Public reads on a bucket of user uploads means anyone who can guess or enumerate a path can download it, including identity documents, selfies and screenshots people believed were private. Firebase's Storage docs show the correct shape: a path segment for the user ID, a write rule of request.auth.uid == userId, and a read rule of request.auth != null or a narrower ownership check. For an unauthenticated request, request.auth is null, so the comparison fails and the download is refused.
The same docs show validation on the way in. A rule such as request.resource.size < 5 * 1024 * 1024 && request.resource.contentType.matches('image/.*') stops a stranger from using your bucket as free hosting for arbitrary files and stops a user uploading a 2 GB video where you expected a profile picture.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{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 < 5 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
match /{allPaths=**} {
allow read, write: if false;
}
}
}Mistake 6: trusting fields the client sends
A rule that only checks who is asking still lets that person write anything. Generated apps routinely store role, isAdmin, credits, price or status on the same document the user is allowed to edit. If the update rule does not inspect request.resource.data, a user can open the browser console and set their own role to admin or their order total to zero.
Firestore's rules-conditions guide draws the line clearly: resource.data is the document as it exists, and request.resource.data is the document as it will exist after the write. Good update rules compare the two. They pin the fields a user must not change to their existing values and they check types and ranges on the fields a user may change.
A practical habit: for every writable collection, list the fields the client is allowed to set, and write the rule so that only those fields can differ between resource.data and request.resource.data. Everything else, such as role, ownerId and createdAt, is either set by a trusted server with the Admin SDK, which bypasses rules, or is pinned in the rule.
match /profiles/{userId} {
allow update: if request.auth != null
&& request.auth.uid == userId
&& request.resource.data.role == resource.data.role
&& request.resource.data.credits == resource.data.credits
&& request.resource.data.displayName is string
&& request.resource.data.displayName.size() <= 80;
}Bad rule, fixed rule
Use this table to scan a generated rules file quickly. Each left-hand pattern is a real snippet you will find in AI-built projects, and the right-hand one is the shape Firebase's documentation recommends.
| Where | Generated rule | Fixed rule |
|---|---|---|
| Firestore | allow read, write: if request.time < timestamp.date(2026, 10, 22); | Per-collection match blocks with request.auth.uid checks, and a final match /{document=**} that denies everything. |
| Firestore | allow read, write: if true; | allow read, write: if request.auth != null && request.auth.uid == userId; |
| Firestore | allow read, write: if request.auth != null; | allow read: if resource.data.ownerId == request.auth.uid; separate create and update rules that check request.resource.data. |
| Realtime Database | ".read": "auth.uid !== null" at the root | "users": { "$uid": { ".read": "$uid === auth.uid", ".write": "$uid === auth.uid" } } |
| Storage | allow read: if true; on match /{allPaths=**} | match /users/{userId}/{file} with request.auth.uid == userId on read and write, plus size and contentType checks. |
| Any | Update rule with no request.resource.data checks | Pin role, ownerId and money fields to their existing values and validate type and size of editable fields. |
How to test rules before you deploy
The Rules Playground lives in the Firebase console under the Rules tab of Firestore, Realtime Database or Storage. You choose a read or write, a path, and an authentication state such as unauthenticated or a specific user ID, then click Run and read the allowed or denied banner. It is the quickest way to answer "can a logged-out visitor read this document?" without writing code.
For anything you will maintain, Firebase recommends the Local Emulator Suite and the @firebase/rules-unit-testing library, which is the only supported way to mock auth in rules tests and never touches production resources. You create a test environment with initializeTestEnvironment, get an unauthenticatedContext and an authenticatedContext for a fake user, and wrap operations in assertSucceeds or assertFails. Run the suite with firebase emulators:exec so the emulators start and stop around it.
Write the negative tests first. The ones that catch generated rules are: unauthenticated read of a user document fails, user B reading user A's document fails, user A changing their own role fails, and an unauthenticated download from the Storage bucket fails. If those four pass in the emulator against the rules file you are about to deploy, you have closed the six mistakes above.
- 1Install: npm install --save-dev @firebase/rules-unit-testing firebase-tools
- 2Point firebase.json at your firestore.rules and storage.rules files.
- 3Write tests with initializeTestEnvironment, unauthenticatedContext and authenticatedContext.
- 4Run: firebase emulators:exec --only firestore,storage "npm test"
- 5Deploy only after the negative tests pass: firebase deploy --only firestore:rules,storage
import { initializeTestEnvironment, assertFails, assertSucceeds } from "@firebase/rules-unit-testing";
import { doc, getDoc, setDoc } from "firebase/firestore";
import { readFileSync } from "fs";
const env = await initializeTestEnvironment({
projectId: "demo-rules-test",
firestore: { rules: readFileSync("firestore.rules", "utf8") },
});
const alice = env.authenticatedContext("alice").firestore();
const bob = env.authenticatedContext("bob").firestore();
const anon = env.unauthenticatedContext().firestore();
await assertSucceeds(setDoc(doc(alice, "users/alice"), { displayName: "Alice" }));
await assertFails(getDoc(doc(anon, "users/alice")));
await assertFails(getDoc(doc(bob, "users/alice")));
await assertFails(setDoc(doc(alice, "users/alice"), { role: "admin" }));
await env.cleanup();Frequently asked questions
Is allow read, write: if request.auth != null secure enough?
No. Firebase's own insecure-rules guide lists it as access for any authenticated user. It stops logged-out visitors, but anyone who signs up, including with anonymous auth, can read and edit every document the rule covers. Add an ownership condition such as request.auth.uid == userId on the path or a comparison with an ownerId field in the document.
How do I know if my Firebase project is still in test mode?
Open the Rules tab for Firestore and Storage in the Firebase console and look for a condition of the form request.time < timestamp.date(...). That line is the test-mode timer. Firebase describes test mode as allowing anyone to read and overwrite your data, so replace the block with collection-specific rules that check request.auth.uid rather than moving the date.
Do Firebase Security Rules protect data accessed by my server?
No. Rules apply to requests from the web and mobile client SDKs. The Firebase Admin SDK and Cloud Functions using it bypass rules entirely, which is why production mode can deny all client access and still let your server work. Anything your server writes, such as roles or balances, should be pinned in the client rules so users cannot change it.
Can I make one Storage folder public and keep the rest private?
Yes. Write a match block for the public path with allow read: if true and no client write, and a separate block for user folders that requires request.auth.uid == userId. Keep a final match /{allPaths=**} that denies everything else. Never put allow read: if true on the bucket-wide wildcard, because in Storage a broader allow rule overrides narrower denies.
What is the fastest way to test Firebase rules?
Use the Rules Playground in the console for a one-off check: pick read or write, a path and an auth state, then run it. For rules you will keep changing, write tests with @firebase/rules-unit-testing against the Local Emulator Suite and run them with firebase emulators:exec. Start with assertFails tests for unauthenticated reads and for one user reading another user's document.
Put it into practice
Sources
- 1.Firebase docs: Basic Security Rules
- 2.Firebase docs: Avoid insecure rules
- 3.Firebase docs: Get started with Cloud Firestore (test mode and production mode)
- 4.Firebase docs: Cloud Storage Security Rules conditions
- 5.Firebase docs: Writing conditions for Cloud Firestore Security Rules
- 6.Firebase docs: Build unit tests for Security Rules