Firebase · 10 min · Updated 28 Aug 2026
Is my Firebase app secure? The rules mistakes that expose everything
Your Firebase config is meant to be public. Security rules are the only control left — here is how to tell whether yours hold.
The first thing to settle, because it is the question everyone asks first: no, finding your Firebase config in your JavaScript is not a leak. The apiKey, projectId and authDomain in that object are identifiers, not credentials, and Google publishes them in its own documentation. They are supposed to be in your bundle.
What follows from that is the part worth your attention. Because the config is public, anyone can point the Firebase client SDK at your project and start making requests. Your database is on the internet, addressable by strangers, by design. The only thing deciding what those requests return is your security rules.
Firestore, Realtime Database and Storage each have their own rules, and they are independent. Locking down Firestore does nothing for Storage. Most exposures we see are not a project with no rules — they are a project where one of the three was tightened and the other two were never revisited.
Read your rules before anything else
Open the Firebase console, then Firestore Database, then Rules. Do the same for Realtime Database and for Storage. You are looking for one pattern, in any of the three.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 9, 15);
}
}
}This is what "Start in test mode" writes, and what a coding agent will happily leave in place because the app works perfectly with it. It is a global allow with an expiry date. Both halves are bad: before the date, every document in your project is world readable and world writable; after it, every request fails and your product goes down.
match /{document=**} {
allow read, write: if request.auth != null;
}This one is more insidious, because it looks like a security control and passes a casual review. It checks that the caller is signed in. It does not check that the document belongs to them. Anyone can sign up for your app in ten seconds, so the practical difference between this and no rules at all is ten seconds.
The rules mistakes coding agents make
Four patterns account for nearly everything we find in AI-built Firebase projects. Each one produces an app that works flawlessly for the person testing it.
Ownership checked on the way in, not on the way out. A create rule verifies request.resource.data.userId == request.auth.uid, so users can only create their own documents — and the read rule never checks resource.data.userId, so they can read anyone's.
A recursive wildcard hiding under a specific rule. A carefully scoped rule on /users/{uid} sits above a leftover match /{document=**}. Rules do not override each other — access is granted if any rule allows it. The broad one wins.
Storage left on the default. Firestore gets the attention because that is where the agent wrote the data model. Uploaded files — avatars, invoices, ID documents — sit in Storage under rules nobody read.
Validation only in the UI. The app sets the role field to user and the rules permit writing any field. A client that writes role: "admin" to its own document is not hacking anything; it is using the API as written.
Every pattern above behaves correctly when the only person testing is the owner of the data. You need a second account, and a document ID that belongs to the first, to see the difference. That is why an external check of a Firebase app can never honestly return "Ready to ship" on authorization — it has not logged in.
The 10-point Firebase checklist
Run this before real customers arrive. Items 01 to 06 you can check from the console in a few minutes; 07 to 10 need two accounts.
- 01No rule in any of the three services allows read or write based only on a date, and no
request.timeexpiry remains anywhere. - 02No rule grants access on
request.auth != nullalone for user-owned data. - 03Read rules compare
resource.dataownership, not justrequest.resource.dataon create. - 04There is no leftover
match /{document=**}allow rule beneath your specific rules. - 05Storage rules are scoped per user path, and files holding customer data are not readable while signed out.
- 06Realtime Database rules were reviewed too, even if you think you are not using it — a project can have both.
- 07Write rules restrict which fields can be set, so a client cannot promote itself by writing a
roleorplanfield. - 08Account A, signed in, cannot read account B's document by its ID through the SDK or the REST API.
- 09Account A cannot update or delete account B's document. Read scoping and write scoping are separate rules.
- 10Any Cloud Function that reads protected data checks the caller itself. The Admin SDK bypasses security rules completely.
Cloud Functions run with the Admin SDK, which is not subject to security rules at all. A callable function that fetches a document by an ID the client supplied, without checking who is calling, is a hole straight through every rule you just tightened.
How to check it yourself, concretely
Two accounts, five minutes. Sign in as account A in your app and create a record. Copy its document ID from the console. Sign in as account B — a second real signup, in a different browser profile — and try to fetch account A's document by that ID.
GET https://firestore.googleapis.com/v1/projects/<project-id>/databases/(default)/documents/invoices/<A-document-id>
Authorization: Bearer <account B's ID token>
# a correctly scoped project answers
HTTP/1.1 403 Forbidden { "error": { "status": "PERMISSION_DENIED" } }
# an exposed project answers with account A's data
HTTP/1.1 200 OK { "fields": { "customer": ... } }Do the same for update and delete, and repeat both against a Storage object. Four requests, and you know something you previously only believed.
After you tighten the rules, prove it
Firebase rules are unusually easy to fix wrongly, in either direction: too loose and nothing changed, too tight and your own app stops loading. So verify both sides. Send account B's request again and confirm it is now refused. Then sign in as account A and confirm your own data still loads.
A finding is not closed because you edited a rule. It is closed when the request that reached another customer's data can no longer reach it — and the only way to know that is to send it again.