How To

How to Audit Firebase Security Rules with Native Tools

A one-hour Firebase security rules audit using only the Firebase CLI, the Rules management API, the Rules Playground, the Emulator Suite and the App Check and Auth consoles. It shows how to judge a grep hit with evidence, and why a point-in-time audit can't catch Thursday's release.

10 min read
Cover Image for How to Audit Firebase Security Rules with Native Tools

How to Audit Firebase Security Rules with Native Tools

Block out one hour, open one Firebase project, and you can get surprisingly far: read every deployed ruleset, spot the if true left over from test mode, confirm App Check is actually enforcing, and check which sign-in providers are live. This guide is that hour, written down: a complete Firebase security rules audit using only the Firebase CLI, the console, the Rules Playground and the Emulator Suite. Nothing to buy, nothing to install beyond firebase-tools.

The catch comes at the end. Everything you verify today is true today. The next firebase deploy from any engineer's laptop can replace your rules, and there's no diff review built into that flow. We'll deal with that honestly in the closing section.

In part one of this series we walked through the misconfigurations that actually expose Firebase apps: open test-mode rules, auth-only-but-any-user reads, missing ownership checks, unenforced App Check. This article assumes you know what you're hunting. Here we cover how, tool by tool.

Work through the passes in this order. Each one narrows what the next has to look at.

01Inventory

Firebase CLI

Every project and app, then the rules actually deployed.

02History

Releases

When access changed, and whether anyone expected it.

03Probe

Playground

One simulated request at a time against live rules.

04Prevent

Emulator tests

Rules assertions that fail a pull request in CI.

05Perimeter

App Check, Auth

Enforcement state, sign-in providers, authorized domains.

One hour, one project, five passes. Nothing to install beyond firebase-tools.

Tool 1: Firebase CLI, inventory first, rules second

You can't audit what you can't list. Start with a full inventory (Firebase CLI docs):

firebase login
firebase projects:list
firebase apps:list --project YOUR_PROJECT_ID

Write down every project and every registered app (iOS, Android, web). At most companies this list is longer than anyone expects: hackathon projects, an abandoned prototype, a contractor's staging environment. Each has its own rules, its own auth config and its own data. The forgotten ones are statistically your riskiest.

For each web app, pull its config to see which project a given bundle actually talks to:

firebase apps:sdkconfig WEB APP_ID --project YOUR_PROJECT_ID

Pull the deployed rules, not the ones in your repo

The rules file in your repository is a claim. The ruleset attached to the live release is the fact. They drift whenever someone edits rules in the console or deploys from a stale branch. Fetch what's actually deployed via the Rules management REST API:

TOKEN=$(gcloud auth print-access-token)

# List active releases (cloud.firestore, firebase.storage/<bucket>)
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://firebaserules.googleapis.com/v1/projects/YOUR_PROJECT_ID/releases"

# Fetch the ruleset source a release points at
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://firebaserules.googleapis.com/v1/projects/YOUR_PROJECT_ID/rulesets/RULESET_ID"

Each release's rulesetName ends in the RULESET_ID for the second call, and the source comes back under source.files[].content. Realtime Database rules live in a separate system. List your instances, then fetch each instance's rules over REST:

firebase database:instances:list --project YOUR_PROJECT_ID

curl -s "https://YOUR_DB_INSTANCE.firebaseio.com/.settings/rules.json?access_token=$TOKEN"

Save everything to files. Then run the sixty-second triage, the patterns from part one as greps:

# The classic open-database patterns
grep -nE 'if\s+true' firestore.rules storage.rules
grep -nE '"\.(read|write)"\s*:\s*true' database.rules.json

# Time-boxed test rules: check whether the date already passed (or is years out)
grep -n 'request.time' firestore.rules storage.rules

# Auth-only-but-any-user: every hit needs a per-document ownership check nearby
grep -n 'request.auth != null' firestore.rules storage.rules

A grep hit isn't automatically a finding. request.auth != null is fine on genuinely shared data. But every hit gets a human decision recorded in your audit doc: intended, or accident. The next two tools are how you make that decision with evidence instead of a guess.

Tool 2: Release history, when did access widen?

The Firebase console keeps a version history of your rules. Open Firestore Database → Rules and use the version list in the left panel of the rules editor to step through previous rulesets and compare them (manage rules docs). Storage has the same view under Storage → Rules, and Realtime Database under Realtime Database → Rules.

Two questions for the history:

  1. When was the last release, and who expected it? A rules release nobody on the team can account for is itself a finding.
  2. Did any release widen access? Diff the current version against the one from your last audit. Look for removed conditions, new allow statements on broad match paths like match /{document=**}, and ownership checks (request.auth.uid == resource.data.ownerId) that quietly disappeared.

The API equivalent lists historical rulesets with their createTime, so you can script "tell me if a ruleset newer than my audit exists":

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://firebaserules.googleapis.com/v1/projects/YOUR_PROJECT_ID/rulesets" \
  | jq -r '.rulesets[] | "\(.createTime)\t\(.name)"'

The rulesets API doesn't record who published. For that, look at Cloud Audit Logs (Admin Activity) for firebaserules.googleapis.com, where release updates are logged with the caller's identity.

Tool 3: Rules Playground, fast spot checks

Before writing tests, use the Rules Playground in the console rules editor (Firestore Database → Rules → Rules Playground) to probe the deployed rules interactively. It simulates a single operation against your rules without touching production data.

Run at least these four simulations against your most sensitive collection:

  • Unauthenticated get on a real document path. It should be denied almost everywhere.
  • Unauthenticated list on the collection. get and list are separate permissions in Firestore rules, and teams routinely lock one and forget the other.
  • Authenticated get as a fabricated UID on a document owned by a different user. The Playground lets you set any uid in the simulated auth payload. This is the test that catches auth-only-but-any-user reads.
  • Authenticated update as that same wrong UID. Write paths deserve the same scrutiny as reads.

The Playground highlights exactly which rule statement allowed or denied the request, which makes it the fastest way to understand why a bad rule is bad.

Walking one grep hit

Say Tool 1 turned up allow read: if request.auth != null; on orders. Three readings fit, and each predicts something different:

01Reading 1

Intended

The collection holds shared data. Documents have no owner field and the team can say why.

02Reading 2

No ownership

Documents carry an ownerId the rule never checks. A fabricated UID reads them.

03Reading 3

Effectively public

Either of the above with anonymous sign-in on. Anyone with the project ID is signed in.

Three readings of one grep hit: request.auth != null on orders. Each predicts a different Playground result.

Check them in order:

  1. Sample a document. Open a few orders documents in the console. If they carry an ownerId or userId field, reading 1 is unlikely: the data model says per-user, and the rule doesn't.
  2. Run the cross-user get in the Playground. Authenticate as a fabricated UID and read a document whose ownerId is someone else. If it's allowed, that's reading 2, confirmed against the deployed rules.
  3. Check the sign-in providers. If Authentication → Sign-in method shows Anonymous enabled, any visitor can get a valid request.auth without an email. That's reading 3, and it raises the severity of whatever reading 2 found.

The tools give a clean answer to each question. Choosing the next question is still you. And the Playground's limit is real: one operation at a time, by hand, in a browser. That's a spot check, not coverage. Coverage is the next tool.

Tool 4: Emulator Suite, rules tests that run in CI

The Emulator Suite plus @firebase/rules-unit-testing turns your audit assertions into a permanent test suite. Install and initialize:

npm install -D @firebase/rules-unit-testing firebase-tools mocha
firebase init emulators   # select Firestore

Then encode the same checks from the Playground as tests (Node, v2 API or later):

const fs = require('node:fs');
const {
  initializeTestEnvironment,
  assertFails,
  assertSucceeds,
} = require('@firebase/rules-unit-testing');

let testEnv;

before(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: 'demo-rules-audit',
    firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') },
  });
});

after(async () => testEnv.cleanup());

it('denies unauthenticated reads of user docs', async () => {
  const anon = testEnv.unauthenticatedContext();
  await assertFails(anon.firestore().doc('users/alice').get());
});

it('denies cross-user reads', async () => {
  const mallory = testEnv.authenticatedContext('mallory');
  await assertFails(mallory.firestore().doc('users/alice').get());
});

it('allows owners to read their own doc', async () => {
  const alice = testEnv.authenticatedContext('alice');
  await assertSucceeds(alice.firestore().doc('users/alice').get());
});

Run the suite against the emulator in one shot. emulators:exec starts the emulator, runs your test command and tears it down:

firebase emulators:exec --only firestore "npx mocha test/rules.test.js --timeout 10000"

Wire that command into CI so a pull request that loosens firestore.rules fails before it deploys. This is the single highest-leverage output of the whole audit: the only native mechanism that catches a rules regression before release. Note what it doesn't cover: rules edited directly in the console never pass through your CI.

Tool 5: App Check, auth providers, authorized domains

Rules decide who can do what. App Check decides which clients get to ask at all. In the console, open App Check and review both tabs:

  • Apps. Every app should have an attestation provider registered (App Attest or DeviceCheck on Apple, Play Integrity on Android, reCAPTCHA Enterprise or reCAPTCHA v3 on web). An app with no provider can't send valid App Check tokens.
  • APIs. This is where teams get burned. Each product (Firestore, Realtime Database, Storage, Authentication) shows whether it's enforced or only monitored. Projects sit in monitoring for months because someone planned to check the metrics and never did. Monitoring blocks nothing.

Then two checks under Authentication:

  1. Sign-in method → providers. Confirm every enabled provider is intentional. Anonymous auth is the one to scrutinize: any rule gated only on request.auth != null is effectively public if anonymous sign-in is on.
  2. Settings → Authorized domains. This list controls where OAuth sign-in flows may complete. Prune stale preview URLs and contractor domains. You can also pull the config programmatically via the Identity Toolkit admin API:
curl -s -H "Authorization: Bearer $TOKEN" \
  -H "x-goog-user-project: YOUR_PROJECT_ID" \
  "https://identitytoolkit.googleapis.com/admin/v2/projects/YOUR_PROJECT_ID/config" \
  | jq '{authorizedDomains, signIn}'

At the end of these five passes you have a findings document per project: rules verdicts with file-and-line references, a release-history note, a CI-enforced test suite, and an App Check and auth checklist.


What native tools cover, and what they leave

Native toolWhat it gives youWhat it still leaves to a person
Firebase CLI and Rules APIThe deployed rules for every projectReading them and judging each hit
Release historyEvery past ruleset, with timestampsNoticing a new release and diffing it
Rules PlaygroundExact allow or deny for one requestRunning enough requests to mean coverage
Emulator tests in CIRegressions blocked before deployConsole edits, which never reach CI
App Check and Auth settingsEnforcement and provider stateChecking it again after someone changes it

Down the right-hand column it's the same gap: Firebase records the facts, and a person has to look, compare and decide, again after every change.

Where the DIY audit stops working

Run this audit. It's worth the hour several times over. But know its structural limits before you call the problem solved.

01Time

Point in time

A console edit on Thursday undoes the audit from Tuesday.

02Scope

One project

Twelve projects is twelve hours, and the forgotten ones get none.

03Signal

No release alarm

Nothing native tells you a new ruleset widened access.

04Action

Tickets

Findings compete with feature work every sprint.

The DIY audit is worth the hour. These four limits are structural, not effort.

It's point-in-time. Firebase rules change through a five-second deploy or a console edit. The Playground session you ran Tuesday says nothing about the release that ships Thursday. Only the emulator tests persist, and only for changes that go through your repo.

It's per-project. Everything above was one project. With twelve projects across staging, prod and the forgotten hackathon tier, it's twelve hours, and the forgotten ones are precisely the ones nobody budgets audit time for.

Regressions land silently. There's no native alert for "a new ruleset release widened access." Between audits, a bad merge or a hurried console edit is invisible until someone notices traffic that shouldn't exist.

Findings aren't fixes. The audit produces tickets. Tickets need owners, and rules tickets compete with feature work every sprint.

What comes next

Every check in this guide can run continuously instead of quarterly. In part three, CloudThinker's security agent connects to Firebase read-only, keeps the inventory current, re-audits Firestore, Storage and Realtime Database rules on every release, and flags the releases that widen access. It acts within Auto Mode, so you decide what runs on its own and what waits for approval, and a corrected ruleset can wait for a named approver before anything publishes.

If you want to see it against your own projects first, ask something like this in your first session:

"Diff the last two Firestore rules releases in the production project and tell me what an anonymous or unrelated signed-in user can now read that they couldn't before. Propose only, change nothing."

Try CloudThinker free with 100 premium credits and no card required, or continue with part three.

firebasesecurityfirestoresecurityrulesappcheckdevsecops