Firebase quickstarts and "test mode" hand you a database and a storage bucket wide
open (.read: true, .write: true) and tell you to come back and lock them down
later. A surprising share of shipped apps never do. Both Firebase Realtime Database and
Firebase Cloud Storage use declarative security rules that are easy to get wrong in
ways that silently expose every user's data to any anonymous caller on the internet.
This guide covers how each exposure happens, how to detect it, and how to lock rules
down to the right scope.
Firebase Realtime Database: the .read: true trap
Firebase Realtime Database is a JSON document store whose security is governed by a
rules file (typically database.rules.json). Rules are evaluated server-side on every
request. The most dangerous rule is also the simplest:
{
"rules": {
".read": true,
".write": true
}
}
This is the default rule shipped with many Firebase quickstart guides and tutorials. When deployed, it grants every unauthenticated internet caller the ability to read and write the entire database. No Firebase account is required, not even an anonymous Firebase Auth session.
The .json enumeration trick
Firebase Realtime Database exposes a REST API. The host depends on when and where the database was created. Older or US-region instances use:
https://<project-id>.firebaseio.com/<path>.json
Databases created more recently, or outside the US, use a region-qualified host on a different domain:
https://<db-name>.<region>.firebasedatabase.app/<path>.json
Both forms behave identically, and any path is fetched by appending .json to it. If
you only probe firebaseio.com, you miss every instance created on the newer host. With
.read: true at the root, a single unauthenticated request dumps the entire database tree:
GET https://<project-id>.firebaseio.com/.json
A 200 response containing a JSON object confirms full anonymous read access. A 401 response indicates authentication is required. The response for a large database can be many megabytes, and the request itself is indistinguishable from a normal app request. RTDB enforces platform-level connection and quota limits, but nothing in the rules language rate-limits an individual path, so there is little to slow a scraper working through an open one.
The auth != null anonymous-auth pitfall
Many developers tighten their rules to:
{
"rules": {
".read": "auth != null"
}
}
This looks restrictive but has a critical nuance: auth != null means any Firebase
Auth user for your project, including users authenticated via Anonymous
Authentication. Firebase custom tokens are project-specific and do not grant
cross-project access, but anonymous auth is a different matter entirely. If your project
has the Anonymous Authentication sign-in provider enabled, any caller can invoke
signInAnonymously() against your project's client SDK, receive a valid token scoped to
your project, and immediately satisfy auth != null. No real account, no password, and
no invitation needed.
So once the Anonymous provider is enabled, "only authenticated users can read" quietly becomes "anyone on the internet can read." And enabling it is common: quickstarts and consumer-app onboarding often turn it on so people can use the app before they create an account.
Firebase Cloud Storage: allow read: if true
Firebase Cloud Storage uses a different rules language, but the same failure mode
applies. Storage rules live in a storage.rules file and are evaluated per
request. An open-rule example:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if true;
}
}
}
This allows any unauthenticated caller to read and write any file in the bucket. Like the Realtime Database equivalent, it is common in tutorial code and often ships to production unchanged.
The same auth != null pitfall applies to Storage rules. The rule:
allow read: if request.auth != null;
grants read access to any authenticated user in this project, including anonymous
sessions. If the Anonymous provider is enabled, a caller can signInAnonymously(),
receive a token scoped to your project, and read every file the rule covers. Profile
photos, user-uploaded files, and application assets are all in reach of someone who
never created a real account.
How to detect open rules
Realtime Database: unauthenticated probe. Without any credentials or Firebase SDK, request the root of the database:
GET https://<project-id>.firebaseio.com/.json
If the response is a JSON payload (even null), the database is readable without
authentication. A 401 with {"error":"Permission denied."} means authentication is
required.
Realtime Database: Firebase console. Open the Firebase console, navigate to
Realtime Database → Rules. If the rules tab shows ".read": true or
".read": "auth != null" at the root and Anonymous Authentication is enabled
(Authentication → Sign-in method → Anonymous → Enabled), the database is effectively
open to unauthenticated callers.
Storage rules: Firebase emulator. The Firebase Local Emulator Suite includes a rules coverage tool. Run:
firebase emulators:start --only storage
Then issue an unauthenticated request against the emulator's storage endpoint and observe whether it succeeds. The emulator's rules debug output will show exactly which rule matched.
Check a Firebase endpoint here. Paste the firebaseio.com or
firebasestorage.googleapis.com URL into the open-bucket viewer to probe
anonymously and see what an unauthenticated caller can access.
What's at risk
When a Firebase Realtime Database is open to unauthenticated reads, a single GET request can return every record stored in the application. Mobile apps frequently store:
- User profiles, including names, email addresses, phone numbers, and home addresses.
- Session tokens and device push-notification identifiers.
- Application state data, purchase histories, and message threads.
- Internal configuration objects, sometimes including third-party API keys hardcoded at database setup time.
Open write rules are the real disaster. An unauthenticated caller who can write to the
database root can overwrite or delete application data, inject malicious content, or
quietly escalate privileges by flipping a role or isAdmin field directly in the
record. The database is the source of truth, and nothing else gets a vote.
Storage exposures leak whatever users uploaded, and in practice that is KYC identity documents more often than anything else. Firebase is the default backend for consumer-fintech MVPs, the kind of app that asks for a passport scan on day one, and those scans land in a bucket the team forgot to lock.
How to lock rules down
Realtime Database: scope reads to individual users.
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
This grants each authenticated user read and write access only to their own subtree
(/users/<their-uid>), not to the entire database.
Storage rules: scope to the authenticated user's path.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{allPaths=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
This restricts access to paths under /users/<uid>/ to the authenticated user who
owns that path, using exact UID matching rather than a broad auth != null check.
Disable Anonymous Authentication if you don't need it. In the Firebase console,
navigate to Authentication → Sign-in method and disable the Anonymous provider if your
app does not require it. This closes the auth != null anonymous-auth gap: no one can
obtain a valid token without a real credential.
Validate rules before deploying. Use the Firebase Rules Playground in the console
(Realtime Database → Rules → Rules Playground) to simulate requests with auth=null
before deploying any rule change. The emulator's rules coverage tool provides the same
capability for local testing.
For an index of exposed Firebase Realtime Database instances found by this service, see the Firebase Realtime Database exposure index. For Firebase Cloud Storage exposures, see the Firebase Storage exposure index. To test a specific Firebase URL right now, use the open-bucket viewer.