How To

How to Audit Firebase Security Rules with Native Tools

A hands-on Firebase security rules audit using only native tools: pull deployed Firestore, Storage, and Realtime Database rules with the Firebase CLI and Rules REST API, review release history in the console, spot-check access with the Rules Playground, turn audit assertions into CI tests with @firebase/rules-unit-testing and the Emulator Suite, and verify App Check enforcement, sign-in providers, and authorized domains. Exact commands and test snippets throughout — plus an honest look at where a point-in-time, per-project DIY audit falls short. Part two of our Firebase security audit series.

·
firebasesecurityfirestoresecurityrulesappcheckdevsecops
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 silently 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.

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. Every one of them 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 projects 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"

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 -n '".read"\s*:\s*true\|".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

# Auth-only-but-any-user: every hit needs a per-document ownership check nearby
grep -n 'request.auth != null' firestore.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.

Tool 2: Console release history — when did access widen?

The Firebase console keeps a version history of your rules. Open Firestore Database → Rules, and use the version selector 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; 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 specifically 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-side equivalent: GET /v1/projects/YOUR_PROJECT_ID/rulesets lists historical rulesets with createTime, so you can script "alert me if a ruleset newer than my audit exists."

Tool 3: Rules Playground — fast spot checks

Before writing tests, use the Rules Playground built into the console rules editor (Firestore Database → Rules → Rules Playground) to interrogate 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 — should be denied almost everywhere.
  • Unauthenticated list on the collection — list and get are separate permissions in Firestore rules; teams routinely lock one and forget the other.
  • Authenticated get as a fabricated UID (the Playground lets you set a fake auth payload with any uid) on a document owned by a different user — 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. Its limit: 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
firebase init emulators   # select Firestore

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

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 — it starts the emulator, runs your test command, and tears 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: it's the only native mechanism that catches a rules regression before release. Note what it does not 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, Play Integrity, reCAPTCHA Enterprise for 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, Functions) shows Enforced, Monitoring, or unenforced. 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" \
  "https://identitytoolkit.googleapis.com/admin/v2/projects/YOUR_PROJECT_ID/config"

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/auth checklist.

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:

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 is 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. 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 ones that widen access — with nothing changed without your approval. That's part three of this series.

Or point it at your own projects first: Try CloudThinker free — 100 premium credits, no card required.