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.
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:
- When was the last release, and who expected it? A rules release nobody on the team can account for is itself a finding.
- Did any release widen access? Diff the current version against the one from your last audit. Look for removed conditions, new
allowstatements on broad match paths likematch /{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
geton a real document path. It should be denied almost everywhere. - Unauthenticated
liston the collection.getandlistare separate permissions in Firestore rules, and teams routinely lock one and forget the other. - Authenticated
getas a fabricated UID on a document owned by a different user. The Playground lets you set anyuidin the simulated auth payload. This is the test that catches auth-only-but-any-user reads. - Authenticated
updateas 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.
Check them in order:
- Sample a document. Open a few
ordersdocuments in the console. If they carry anownerIdoruserIdfield, reading 1 is unlikely: the data model says per-user, and the rule doesn't. - Run the cross-user
getin the Playground. Authenticate as a fabricated UID and read a document whoseownerIdis someone else. If it's allowed, that's reading 2, confirmed against the deployed rules. - Check the sign-in providers. If Authentication → Sign-in method shows Anonymous enabled, any visitor can get a valid
request.authwithout 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:
- Sign-in method → providers. Confirm every enabled provider is intentional. Anonymous auth is the one to scrutinize: any rule gated only on
request.auth != nullis effectively public if anonymous sign-in is on. - 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 tool | What it gives you | What it still leaves to a person |
|---|---|---|
| Firebase CLI and Rules API | The deployed rules for every project | Reading them and judging each hit |
| Release history | Every past ruleset, with timestamps | Noticing a new release and diffing it |
| Rules Playground | Exact allow or deny for one request | Running enough requests to mean coverage |
| Emulator tests in CI | Regressions blocked before deploy | Console edits, which never reach CI |
| App Check and Auth settings | Enforcement and provider state | Checking 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.
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.
