Managed backends made it possible to ship a real app without a server. They also made it possible to ship a real app with a database anyone on the internet can read and write. With Firebase, the thing standing between those two outcomes is a short rules file, and the version of it most tutorials leave you with is the open one.
The headline needs one honest correction. Firebase rules are not open in every case, and Firebase does tell you. The trouble is when: once, in a setup dialog, while you are trying to get a first read to work. This post covers when the rules are open, what the expiring test rule does, whether the API key in your bundle matters, and how to check what is actually deployed.
Are Firebase security rules open by default?
Only if you choose test mode. When you create a Cloud Firestore database, a Realtime Database or a Cloud Storage bucket in the Firebase console, you pick a starting mode. Locked mode, which Firestore calls production mode, denies client access. Test mode lets anyone read and write your data for a limited period.
Firebase's rules documentation puts the choice in one line: you decide whether your rules "restrict access to your data (Locked mode) or allow anyone access (Test mode)." So the accurate version of the title is that the rules are open by default on the path nearly every tutorial sends you down. The Firestore quickstart says test mode "allows anyone to read and overwrite your data," then tells you to select it if you are starting with the web, Apple or Android SDK. For a client app, the documented starting point is the open one.
What each mode does, product by product:
- Cloud Firestore. Production mode, in the quickstart's words: "Denies all reads and writes from mobile and web clients." Test mode allows every read and write until an expiry date written into the rule itself.
- Realtime Database. Locked mode denies all reads and writes from mobile and web clients. Test mode is world-readable and world-writable for a trial period. If you leave it unchanged, Firebase says "you will be alerted by email, then your database rules will deny all requests."
- Cloud Storage. The same choice exists. Firebase's release notes for January 2022 record that test mode rules for new buckets "now only grant access to untrusted clients for a month," matching Firestore and Realtime Database. The docs disagree with themselves on the restricted starting rule (one page shows deny-all, another says signed-in users only), so read what you actually have.
In every product the server-side Admin SDK ignores these rules, which makes locked mode a good permanent setting for an app that only reaches its data through your own backend.
What test mode actually deploys
This is the rule in the Firebase CLI's starter firestore.rules template. The date is filled in as 30 days from the day you initialise the project:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 11, 1);
}
}
}
The comment that ships above it in the template is blunt: the rule is "configured to expire after 30 days because it leaves your app open to attackers." After that date every client request is denied until you publish new rules.
That expiry is a real safety net, and it fails closed. The damage comes from how people respond. The app stops loading on day 31, and the fastest way to clear the error is one of these:
// Firestore, with the expiry check deleted
allow read, write: if true;
// Realtime Database
{
"rules": {
".read": true,
".write": true
}
}
Firebase's own sample of the first one carries the comment "NEVER use this ruleset in production; it allows anyone to overwrite your entire database." The other popular fix is to push the date a year out, which is the same rule with a longer fuse. Both are how a prototype default becomes a production configuration.
Are Firebase API keys safe to expose?
Yes, for the keys Firebase creates for its own services. Firebase's documentation says API keys restricted to Firebase services "do not need to be treated as secrets, and it's safe to include them in your code or configuration files." They identify your project. They do not authorize anything. Security Rules and App Check do that.
The same page is explicit that API keys for Firebase services "are not used to control access to backend resources." So finding apiKey and projectId in your bundle is expected. It is the same arrangement as the Supabase anon key, which is safe to expose only when Row Level Security is on.
Firebase lists the exceptions, and they matter:
- The Gemini Developer API key. Firebase says it "should never be included in your code or configuration files."
- Service account private keys, used by the Admin SDK. They bypass your rules completely and "must be kept secret."
- FCM server keys from the legacy Cloud Messaging HTTP API, which Firebase also calls sensitive.
- Any key for a non-Firebase Google Cloud API. Firebase recommends a separate, restricted key for each.
If one of those is in your bundle or repository, that is a different and more urgent problem, covered in how indie apps leak their API keys.
What can someone do with open rules?
Everything your own app can do, without using your app. Firebase's documentation puts it plainly: if you are not authenticating users and configuring rules, "anyone who guesses your project ID can steal, modify, or delete the data." No login is involved, and the project ID is one your client already publishes.
The client SDK is a convenience, not a gate. Realtime Database exposes every path as a REST endpoint, and Firebase's reference notes that unauthenticated requests "will only succeed if the Realtime Database Rules allow public read or write access to the data." Firestore's REST API works the same way: for unauthenticated requests it "uses your Cloud Firestore Security Rules to determine if a request is authorized." With test rules in place, one curl command returns the collection. Not just one user's data. Everyone's.
This has been measured at scale. In a write-up on env.fail, researchers MrBruh, xyzeva and Logykk describe scanning for Firebase projects with misconfigured rules and finding 900 sites exposing roughly 125 million user records, including about 106 million email addresses and 20 million passwords. Of the site owners they emailed, by their own count 24% fixed the misconfiguration.
Your project ID and API key ship inside your app. Assume an attacker already has both, and that your rules are the only thing left for them to get past.
Open writes add a second problem. Anyone can overwrite or delete records, and their traffic counts against your project's quota and billing, the pattern covered in denial of wallet attacks on serverless bills.
Rules that look locked down and are not
Replacing if true is not the end of the check. These three states pass a glance and fail a stranger.
Any signed-in user
allow read, write: if request.auth != null;
Firebase files this under insecure rules and asks you to "confirm that you want any logged-in user to have access to the data." If your app lets anyone create an account, signed in is a category that includes the attacker. One free sign-up later they can read every other user's documents.
A leftover catch-all
Firestore and Storage rules are additive. Firebase's documentation states that when more than one match statement applies to a request, "the access is allowed if any of the conditions is true." A carefully scoped rule for /users/{userId} does nothing if the original match /{document=**} block from test mode is still sitting above it. Delete the catch-all instead of writing around it.
Realtime Database rules that try to revoke
Realtime Database rules cascade downward. A rule on a child path "can only grant additional privileges," so a ".read": true at the root cannot be taken back by a false deeper in the tree. Grant narrowly at the leaf, never broadly at the top.
How do you lock down Firebase rules before launch?
Start from deny, then grant each path only to the user who owns it. Tie every read and write to request.auth.uid, validate the shape and size of incoming data inside the rules, and remove any catch-all match left over from test mode. Then deploy, and confirm with an unauthenticated request that access is refused.
Default to deny, and explicitly allow only what each user needs. Never rely on client-side checks alone. The client is not a trust boundary. For Firestore, an owner-only collection with basic validation looks like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/notes/{noteId} {
allow read, delete: if request.auth != null
&& request.auth.uid == userId;
allow create, update: if request.auth != null
&& request.auth.uid == userId
&& request.resource.data.title is string
&& request.resource.data.title.size() <= 200;
}
}
}
The Realtime Database equivalent, following Firebase's content-owner pattern:
{
"rules": {
"users": {
"$uid": {
".read": "auth !== null && auth.uid === $uid",
".write": "auth !== null && auth.uid === $uid"
}
}
}
}
And for Storage, owner-only paths with a size and type limit on uploads:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /user/{userId}/{fileName} {
allow read, delete: if request.auth != null
&& request.auth.uid == userId;
allow create, update: if request.auth != null
&& request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
}
}
Firebase's security checklist makes a point worth repeating: "Don't write security rules after you write your app, as a kind of pre-launch task." Treat rules like a schema, and write the rule when you add the collection. The wider version of this pass is in the checklist to run before you make your backend public.
Verify the deployed rules, not the ones you remember
It is easy to think you tightened the rules and be wrong. The file in your repository and the rules that are live can differ, and Firebase notes that the console "always shows the most recently deployed version." Check the deployed rules directly, and confirm an unauthenticated request actually gets rejected. "I set it up in the console once" is not verification.
Two requests from a terminal, with no credentials at all:
# Realtime Database: expect a permission error, not JSON data
curl "https://YOUR_DATABASE_NAME.firebaseio.com/.json?shallow=true"
# Firestore: expect PERMISSION_DENIED, not a list of documents
curl "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/users"
Realtime Database instances outside us-central1 use a URL ending in firebasedatabase.app. Repeat the Firestore request for each top-level collection. Once the rules are right, deploy them from the CLI so they live in version control:
firebase deploy --only firestore:rules
firebase deploy --only database
firebase deploy --only storage
Firebase says Firestore rule updates can take up to a minute to affect new queries and up to 10 minutes to reach active listeners, so wait before you retest. The Rules Playground in the console and the Local Emulator Suite let you test as a signed-in user, as a different user, and as nobody.
The pre-launch pass, in order:
- Open the Rules tab for Firestore, Realtime Database and Storage. Read what is actually deployed.
- Search all three for
if true,".read": true,".write": trueandtimestamp.date. Each one is a finding. - Search for
auth != nullwith nothing after it, and decide whether every signed-in user should really have that access. - Delete any
match /{document=**}ormatch /{allPaths=**}block that grants access. - Run the two
curlrequests above with no credentials. Data in the response is a finding. - Sign in as one test user and try to read another user's record.
- Search your client bundle and repository for service account JSON and any non-Firebase API key.
Scanning for this before you ship
You can run that whole list by hand in an evening. If you would rather have something read your source first, the free IOnclad browser scanner runs a 167-rule subset entirely in your browser, with no signup. Your code stays in the tab.
The IOnclad desktop app runs 24 scanners and 500+ checks across secrets and git history, dependencies, the OWASP web categories, session and auth, an API surface map, and a denial-of-wallet check. It returns one Ship-It verdict with the file, line and a fix for each finding. It does not promise your app is secure. A scanner reads the files you give it, so the unauthenticated request against your live project is still yours to run.
For the rest of the launch surface, the app security guides collect the related checks in one place. Whichever route you take, remember what the expiry date in the test rule is for. It is a deadline for writing real rules. Extending it is not meeting it.