LEAKCADE

Firebase security rules: fix test mode, if true and open access

Updated 7 min read

Firebase security rules left in test mode or set to allow read, write: if true let anyone with your project ID read, change or delete your data. Require sign-in and match request.auth.uid (the signed-in user's ID) to an owner field, a field on each document that stores its creator's ID. Don't extend the test mode date. Prove your rules in the Firebase emulator, a local copy of Firebase.

QUICK CHECKLIST

  • Search your rules for if true and ".read": true
  • Replace test mode with owner rules on request.auth.uid
  • Never push the test mode date out again
  • Make every query carry the constraint its rule requires
  • Keep admin flags out of client-writable documents
  • Run emulator tests before every deploy

What Firebase security rules do and why test mode is risky

Firebase security rules decide who can read and write your Firestore documents, Realtime Database nodes and Cloud Storage files. Your app talks to Firebase straight from the client with no server of yours in between, so the rules are the only gate.

When you create a database or bucket, the console asks you to pick Locked mode (deny everyone) or Test mode (allow everyone). Test mode is fine for an afternoon. Google's docs warn that without authentication and rules, anyone who guesses your project ID can steal, modify or delete the data.

Test mode loop vs owner rulesTwo lanes. Wrong: open test mode, the date passes, the app breaks, the date is extended and data stays open. Right: sign in, match user ID to owner, emulator tests, closed rules deployed.TEST MODE LOOP1Test mode: openuntil a date2Date passes, appgets denied3Push the dateout again4Data stays opento anyoneOWNER RULES1Sign users inwith FirebaseAuth2Matchrequest.auth.uidto owner3Test in theemulator4Deploy rules,data staysclosedTEST MODE LOOP1Test mode: open until a date2Date passes, app gets denied3Push the date out again4Data stays open to anyoneOWNER RULES1Sign users in with Firebase Auth2Match request.auth.uid to owner3Test in the emulator4Deploy rules, data stays closed
Extending the date repeats the problem. Owner rules end it.

How open Firebase rules get abused

Your Firebase web config sits in your frontend bundle, and anyone can copy the project ID from dev tools. That's by design, the same way NEXT_PUBLIC_ environment variables are public by design. The rules do all the protecting.

With allow read, write: if true, an attacker needs no account. They send requests to Firebase's REST API (plain web requests) or use the SDK with your project ID, then read any collection whose name they can find. For Realtime Database, the docs show the REST address as https://<database_name>.firebaseio.com/path/to/my/data.json.

Writes work the same way. Google's own comment on that ruleset says it lets anyone overwrite your entire database. Open Storage rules expose every file in the bucket.

This is what happens after your Firebase test mode expires. The console's test mode rules include a date check, something like request.time < timestamp.date(2026, 12, 1). Realtime Database test mode uses a similar now < check. After that day, every request is denied and your app throws permission errors.

The quick fix is to push the date out or swap in true. Both reopen your data to the internet. If you paste the error into an AI tool, check that its answer isn't this edit.

request.auth != null looks safer, but Google lists it as an insecure pattern. Every signed-in user passes, and if sign-up is open, that means everyone.

Firebase security rules before and after

Here's the open version next to an owner-only version for a stories collection. Each document stores its creator's user ID (UID) in author_uid, and the rules compare it with request.auth.uid.

BEFOREOpen rules (test mode)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // Anyone can read and write everything
      allow read, write: if true;
    }
  }
}
AFTEROwner-only rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyId} {
      allow read, delete: if request.auth.uid == resource.data.author_uid;
      allow create: if request.auth.uid == request.resource.data.author_uid;
      allow update: if request.auth.uid == request.resource.data.author_uid
                    && request.auth.uid == resource.data.author_uid;
    }
  }
}
Anything not matched by a rule is denied by default, so add a match block for every other collection your app uses before you deploy, or those features will break.

If some data must be public, open only reads (allow read: if true) and keep writes tied to the owner, as in Google's mixed public and private example.

Realtime Database rules need the same treatment. A ".read": true or ".write": true at the top has the same effect as if true. Rules there cascade, so a rule at a parent path overrides stricter rules below it.

{
  "rules": {
    "some_path": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid",
        ".write": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

For Storage, put files under a path that contains the user's UID, compare it with request.auth.uid, and check request.resource.size and request.resource.contentType on writes. The same idea applies to Supabase buckets, covered in Supabase storage policies.

RuleWho gets inVerdict
allow read, write: if trueAnyone on the internetNever ship
request.auth != nullAnyone who signs upOnly for data every signed-in user may see
request.auth.uid == resource.data.author_uidThe document's ownerGood default
get(...users/uid).data.admin == trueUsers flagged adminSafe only if clients can't set the flag

Two traps: rules are not filters, and admin flags

Once your Firestore rules use request.auth.uid, some queries that used to work will fail. Rules are not filters. Firestore checks what a query could return before it runs. If that could include a document you're not allowed to read, the whole request fails, even when every document that exists is yours.

import { collection, getDocs, query, where } from "firebase/firestore"

// Fails: could include other people's stories
await getDocs(collection(db, "stories"))

// Works: the query carries the same constraint as the rule
await getDocs(query(collection(db, "stories"), where("author_uid", "==", user.uid)))

The second trap is the admin flag. Google's docs show role-based rules that read a users document with get(), which works only if clients can't write the role field. If users can update their own document freely, anyone can set admin: true on themselves and your admin rule trusts it.

match /users/{userId} {
  allow read: if request.auth.uid == userId;
  allow create: if request.auth.uid == userId
                && !('admin' in request.resource.data);
  allow update: if request.auth.uid == userId
                && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['admin']);
}
match /announcements/{id} {
  allow read: if true;
  allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true;
}

Set the flag from server code or the console. Custom claims are another option: small values stored on the user's sign-in token and readable as request.auth.token. Your pages and API routes need the same protection (see exposed admin routes).

How to fix Firebase security rules, step by step

Do this for Firestore, Realtime Database and Storage.

  1. Read what's deployed

    Open the Rules tab for each product in the Firebase console. Search for if true, ".read": true, allow read, write;, request.time < (Firestore/Storage test mode) and now < (Realtime Database test mode).

  2. Give every document an owner

    Write the creator's UID (author_uid or similar) when you create the document. Rules need it to decide who owns what.

  3. Replace test mode with owner rules

    Use request.auth.uid against the owner field, and open read only on collections that must be public. Don't touch the date.

  4. Lock role and plan fields

    Clients must not write admin, role or plan on their own documents. Set them from server code or custom claims. Server libraries and the Admin SDK bypass rules entirely, so check the user yourself in those routes (see IDOR in API routes).

  5. Test, then deploy from files

    Keep rules in files under version control and deploy with firebase deploy --only firestore,storage,database. Deploying from the CLI overwrites whatever is in the console.

Test it yourself

Run these only against your own project. Start with the emulator, which never touches production.

  1. Install the tools: npm i -D @firebase/rules-unit-testing firebase vitest, then firebase init emulators.
  2. Add a test that acts as the owner, another user and a signed-out visitor:
import { readFileSync } from "node:fs"
import { afterAll, beforeAll, test } from "vitest"
import { assertFails, assertSucceeds, initializeTestEnvironment, type RulesTestEnvironment } from "@firebase/rules-unit-testing"
import { doc, getDoc, setDoc } from "firebase/firestore"

let env: RulesTestEnvironment
beforeAll(async () => {
  env = await initializeTestEnvironment({
    projectId: "demo-rules-test",
    firestore: { rules: readFileSync("firestore.rules", "utf8") },
  })
})
afterAll(() => env.cleanup())

test("owner rules", async () => {
  await env.withSecurityRulesDisabled(async (ctx) => {
    await setDoc(doc(ctx.firestore(), "stories/s1"), { author_uid: "alice" })
  })
  const alice = env.authenticatedContext("alice").firestore()
  const bob = env.authenticatedContext("bob").firestore()
  const anon = env.unauthenticatedContext().firestore()
  await assertSucceeds(getDoc(doc(alice, "stories/s1")))
  await assertFails(getDoc(doc(bob, "stories/s1")))
  await assertFails(getDoc(doc(anon, "stories/s1")))
  await assertFails(setDoc(doc(bob, "stories/s1"), { author_uid: "bob" }))
})
  1. Run firebase emulators:exec --only firestore "npx vitest run".

A good result is a passing run. A bad result is an assertFails line that fails because the request succeeded. That rule is open. Add a similar test that tries to write admin: true on a user's own document. Load the rules file explicitly, as above. If no rules are loaded, the emulator can treat the project as open and your tests pass for the wrong reason.

Then check the deployed project from outside:

# Realtime Database (use your own database name)
curl "https://YOUR_DATABASE.firebaseio.com/.json?shallow=true"

# Firestore (use your own project ID)
curl "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/stories?pageSize=1"

If you get JSON with your data back, the rules are open. A permission denied error only proves signed-out access to that collection is blocked. Rely on the emulator tests for signed-in users, and repeat the check for each collection and Storage.

Fix prompt you can paste

Give this to Cursor or Claude Code and read the diff before you deploy.

FIX PROMPT
Audit every Firebase Security Rules file in this repo: firestore.rules,
storage.rules, database.rules.json and firebase.json. List with file and
line: `allow read, write: if true`, `allow read, write;` in Storage,
".read": true / ".write": true in Realtime Database, rules whose only
condition is request.auth != null, `request.time <` or `now <` date checks
left over from test mode, and any rule that lets a client write admin,
role, isAdmin or plan fields on their own user document.

Replace them with owner-based rules using request.auth.uid and the owner
field the code actually writes. Keep public read
only on collections that must be public. Do NOT extend any date. Gate
admin access with custom claims or a users document clients cannot write.

Also find client queries (getDocs on a whole collection) that don't carry
the constraint the rule requires, and fix the query, not the rule.

Add Vitest tests with @firebase/rules-unit-testing: owner succeeds, other
user fails, signed-out fails, user cannot set their own admin flag.
Before editing, output a table of every finding with file, line number
and why it is open. After editing, list each changed file and line range
with the old and new rule, explain each change in plain words, and tell me
which rules to deploy.

If you'd like a second check on your repo afterwards, Leakcade can scan it.

Further reading

  1. Firebase docs: Avoid insecure rules
  2. Firebase docs: Get started with Cloud Firestore Security Rules
  3. Firebase docs: Securely query data (rules are not filters)
  4. Firebase docs: Build unit tests for Security Rules

FAQ

My Firebase test mode expired. What should I do?

Replace the date-based rule with owner rules that require sign-in and compare request.auth.uid to an owner field. Don't push the date out or switch to if true, because both leave your data open.

Is 'allow read, write: if true' ever okay?

Not for writes. Google's docs say never to use it in production because it lets anyone overwrite your entire database. If a collection must be public, open only allow read: if true and tie writes to the owner.

Why does my Firestore query fail after I added request.auth.uid rules?

Rules are not filters. Firestore checks whether the query could return documents you can't read, and fails the whole request if so. Add the same constraint to the query, for example where("author_uid", "==", user.uid).

Is it safe that my Firebase config is visible in the browser?

The web config only identifies your project and grants no access by itself. Your security rules decide who can read and write, so with open rules anyone who copies the config can read your data. Keep real secrets such as service account keys out of client code.

Do Firebase security rules apply to the Admin SDK?

No. Server client libraries and the Admin SDK bypass Firestore rules and are governed by Google Cloud IAM (account permissions) instead. Any API route that uses them needs its own check of who the caller is and what they may touch.

Want a second pair of eyes?

We review your whole repo and write a fix prompt for every finding. $19, report within an hour.

Check my repo

Leakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms

Patch Notes by Leakcade

A short weekly email: what changed in AI coding tools, one real security mistake, one prompt you can reuse.