Firebase Security Rules: How to Lock Down an Exposed Realtime Database or Firestore
If your Firebase database is readable by anyone on the internet, the cause is almost always one thing: your security rules allow unauthenticated reads. The fix is to require authentication in the rules — '.read': 'auth != null' for the Realtime Database, allow read: if request.auth != null for Firestore. And the thing that surprises most people: the Firebase API key in your client config is not a secret and has nothing to do with this. The security boundary is your rules, not your key.
This guide covers both Firebase databases — Realtime Database and Cloud Firestore — because they use different rules languages and securing one does not secure the other. It is the Firebase counterpart to the same problem in Postgres-backed apps; if you're on Supabase, the Supabase security guide is the sibling read, because Row Level Security plays the exact role Firebase rules play here.
Why is my Firebase database publicly readable?
A Firebase Realtime Database is exposed when an unauthenticated request can read its data. Concretely: anyone who knows your project's database URL can hit https://<project>.firebaseio.com/.json and get back the entire tree as JSON — no login, no token, no friction. The project URL isn't secret; it's discoverable from your shipped client code.
The reason that request succeeds is your rules. The classic mistake looks like this:
{
"rules": {
".read": true,
".write": true
}
}
".read": true means "allow everyone to read, always." This rule is sometimes pasted in deliberately to "make it work" during development and then forgotten, and sometimes it's the residue of test mode, where Firebase starts you with permissive rules so you can prototype without fighting permissions. Either way, the result is a database anyone can dump — and, with ".write": true, modify or wipe.
Open Firebase databases have been a widespread, recurring real-world exposure for years, almost always from this same root cause: rules left open during development and shipped to production. It isn't an exotic attack. It's a default that was never tightened.
How do I require authentication in Realtime Database rules?
In the Firebase console, go to Realtime Database → Rules. The minimum fix is to require an authenticated user for every read and write:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
auth != null is true only when the request carries a valid Firebase Authentication token. Every anonymous request is now denied. This is the canonical pattern documented in the Firebase Realtime Database security rules guide.
But auth != null alone is blunt: it lets any logged-in user read all the data. For most apps that's still too much. The better pattern is to scope rules per path so each user can only touch their own node:
{
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "auth != null && auth.uid === $uid"
}
}
}
}
Here $uid is a wildcard that captures the path segment, and the rule grants access only when the authenticated user's own UID matches it. A logged-in attacker can read their own data and nothing else. That's the principle to internalize: never deploy ".read": true, and prefer the narrowest rule that still works. Rules cascade — a permissive rule higher in the tree grants access to everything below it, so a stray ".read": true near the root undoes every careful rule beneath it.
After you tighten the rules, treat any data that was exposed as compromised: audit what was reachable, and rotate anything sensitive (tokens, keys, personal records) that an unauthenticated request could have pulled while the database was open.
Are Firestore security rules the same as Realtime Database rules?
No — and this trips people up. Cloud Firestore uses a completely different rules language from the Realtime Database. Realtime Database rules are a JSON tree of .read/.write expressions; Firestore rules use a match/allow syntax. A project can use both databases, and securing one does nothing for the other.
In the Firebase console under Firestore → Rules, the locked-down equivalent of "require auth" looks like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
request.auth != null is Firestore's analogue of auth != null — it's true only for authenticated requests. As with the Realtime Database, the blunt version above lets any signed-in user reach everything, so scope it per document path instead. A per-user rule reads like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}
The official Firestore security rules guide is the canonical reference. The key takeaways: rules_version = '2' is the modern version you want, {document=**} is a recursive wildcard that matches everything below a path (be careful with it — it's the Firestore equivalent of a too-broad root rule), and matches are evaluated independently, so a permissive match block can override a stricter one.
Firestore test mode is the quiet version of the same bug
When you create a Firestore database, Firebase offers test mode, which generates rules that allow open read and write until a future date:
allow read, write: if request.time < timestamp.date(2026, 1, 1);
Test mode is meant as a short development convenience, but it has two failure modes. First, until that date arrives, the database is wide open to anyone — exactly the same exposure as ".read": true. Second, those rules silently lock down when the date passes, which can break a live app that was relying on them, prompting a panicked re-loosening back to open. Either way, test-mode rules that were never replaced with real auth-scoped rules are a public database waiting to be found. Replace them before you ship.
Isn't the Firebase API key the thing I need to protect?
No — and this is the single most important clarification in Firebase security, because it's the one people get backwards.
The Firebase API key in your client config is not a secret. It's the apiKey value in the config object you copy into your web app:
const firebaseConfig = {
apiKey: "AIza...", // not a secret — safe to ship
authDomain: "myproject.firebaseapp.com",
databaseURL: "https://myproject.firebaseio.com",
projectId: "myproject",
// ...
};
That key identifies your project; it does not grant access to your data. It's designed to ship in your frontend, it's visible in DevTools, and that's intentional. Restricting it, rotating it, or trying to hide it does nothing to protect an open database — an attacker reading firebaseio.com/.json doesn't even need it.
This is exactly the same publishable-vs-secret distinction as Supabase's anon key, which we cover in detail in the Supabase security guide: a key that is meant to be public, where the real security boundary is the access-control layer behind it. For Supabase that layer is Row Level Security; for Firebase it's your security rules. If you take one sentence from this post: the rules are the boundary, not the key. A common trail to leaked real secrets is covered in how to find exposed API keys on your website — but the Firebase web apiKey is not one of them.
This confusion shows up constantly in AI-generated and "vibe-coded" apps, where a tutorial snippet wires Firebase straight into a component with test-mode rules and a published key, and everything "works" — see the security risks of vibe coding for why that pattern is so common, and so dangerous.
How do I verify my Firebase rules are actually safe?
Here's the uncomfortable part: you cannot tell from your client config — or even from reading the rule text in the console — whether your database is actually safe. Rules cascade, wildcards behave in non-obvious ways, a permissive block can override a strict one, and a Firestore project can be locked down while its Realtime Database is wide open (or vice versa). The only ground truth is what an unauthenticated request actually returns.
The quickest manual check is to make an anonymous request and confirm it's denied. For a Realtime Database:
curl -s "https://<project>.firebaseio.com/.json"
If that returns your data instead of a Permission denied error, your rules are open. (A null or permission error is what you want.) Do the equivalent for Firestore via its REST API against a path your app uses, with no auth token.
For a real verdict that covers the database your site actually references, run an external scan. SteelSuit's full scan includes a cloud / SaaS-misconfiguration check — the same one that covers Firebase open-database, Supabase RLS-off, and public-bucket listing. The important property is that it's zero false-positive by construction: it flags an exposure only when an actual unauthenticated read of real data succeeds against a database your site references. It proves the exposure by performing the unauthorized read, then links the finding back to the database that returned it — and it does not store the exposed data itself. So a flag means "an anonymous request really did pull your data," not "this looks risky."
That check runs as part of the deeper scan (after a one-time DNS-TXT proof you own the domain), alongside the broader external posture from the web app security checklist — TLS, headers, exposed files, and leaked secrets in your shipped JavaScript.
The takeaway
A Firebase database is exposed for one reason: rules that allow unauthenticated reads. Fix it by requiring auth — '.read': 'auth != null' in the Realtime Database, allow read: if request.auth != null in Firestore — and prefer per-path rules so each user reads only their own data. Remember that the two databases speak different rules languages, that test mode is the quiet version of the same open-read bug, and that the API key in your config is not the boundary. The rules are.
And don't trust the config to tell you you're safe. The only proof is an unauthenticated request that comes back empty. Test that — by hand, or with a scan that performs the read for you and shows you exactly what an anonymous stranger can see.
Frequently asked
Why is my Firebase database publicly readable?
Because its security rules allow reads from unauthenticated requests. In the Realtime Database the classic cause is '.read': true (or test-mode rules that were never tightened); in Firestore it's test-mode rules that defaulted to allow-until-a-date and then either expired or were left wide open. Anyone who knows your project's database URL can then read the data without logging in. The fix is to require auth in the rules — it is not about the API key.
Is the Firebase API key in my client config a secret?
No. The Firebase API key shipped in your web app's config object is not a secret and is fine to publish — it identifies your project, it does not grant access to your data. Like Supabase's anon key, the real security boundary is your security rules, not the key. Restricting or rotating the key does not protect an open database; fixing the rules does.
How do I require authentication in Firebase Realtime Database rules?
In the Firebase console under Realtime Database → Rules, set '.read' and '.write' to 'auth != null' instead of true. That denies every unauthenticated request. Better, scope rules per path so each user can only read their own node, e.g. 'users': { '$uid': { '.read': 'auth != null && auth.uid === $uid' } }. Never deploy '.read': true.
Are Realtime Database and Firestore security rules the same?
No — they use different rules languages. Realtime Database rules are a JSON tree with '.read' and '.write' expressions. Cloud Firestore uses a separate match/allow syntax (rules_version = '2'; match /databases/{db}/documents { match /... { allow read, write: if request.auth != null; } }). Securing one does not secure the other, and a project can use both.
What is Firestore test mode and why is it dangerous?
When you create a Firestore database in test mode, Firebase generates rules that allow open read and write until a future date. Those rules are meant as a short development convenience, but they leave the database wide open until they expire — and if they were copied, extended, or never replaced with real rules, the database stays public. Always replace test-mode rules with auth-scoped rules before going live.
How can I tell if my Firebase rules are actually safe?
You can't tell from the client config or the console rule text alone — you have to test what an unauthenticated request actually returns. Make an anonymous request against your database URL and confirm it is denied. An external scan can do this for you: it performs an unauthorized read against the database your site references and flags it only if real data comes back.