Firebase Security Rules check: the open rule Firebase won't warn you about
Firebase emails you about test-mode rules, but not about "any signed-in user can read everything", and anyone can sign in. Five checks, with copy-paste commands and rules, to find out whether your Firestore, Realtime Database and Storage are really locked.
If your app runs on Firebase, the web config in your JavaScript (apiKey, projectId, storageBucket and friends) is public. That is by design. Anyone who opens your site can copy it and talk to your database directly, without your app's buttons in the way. The only thing standing between that stranger and your users' data is a short file called Security Rules.
AI builders and coding tools (Firebase Studio, Cursor, Replit, v0, FlutterFlow, Claude Code) are good at getting Firebase working. "Working" usually means the rules let the app read and write, and that is often all they check.
How big is the problem?
In January 2026 a researcher published Firewreck 2.0: he pulled the Firebase config out of the top 1.5 million Android apps, tested them, and found 11,126 insecure Firebase projects exposing about 1.43 billion user records, including apps with 100 million downloads. He emailed about 9,900 developers. A month later, 228 projects had been fixed.
The most useful detail for you is in Google's reply to his report. Firebase sends warning emails and shows a console banner when rules are fully open (if true). But, in Google's words, "there is no console warning for open auth rules": rules that let any signed-in user read or write everything. Google closed the report as intended behaviour.
So a quiet inbox does not mean your rules are safe. Here is how to check them yourself.
The three rules that leak
1. Test mode that never left. When you create a database in test mode, Firebase writes a rule like this:
match /{document=**} {
allow read, write: if request.time < timestamp.date(2026, 11, 19);
}
Until that date, anyone on the internet can read, change and delete every document. Firebase emails you before it expires, and the common "fix" (on Stack Overflow, and from AI tools asked to "make the error go away") is to push the date forward. That keeps the database open. The Realtime Database version looks like ".read": "now < 1795046400000", and Storage has its own version too.
2. if true. The fully open rule. Firebase warns about this one, but it still ships, often because an AI tool hit a permission error and took the shortest route.
3. if request.auth != null. This is the one nobody warns you about:
match /{document=**} {
allow read, write: if request.auth != null;
}
It reads as "only logged-in users". But if your app lets people sign up (email and password, Google, or anonymous sign-in), anyone can become a logged-in user in a few seconds, with your public API key and one HTTP request. They don't need your sign-up page. After that they can read every user's documents, not just their own, and often change them too.
The rule you actually want almost always looks like "only the owner of this document": request.auth.uid == userId or resource.data.ownerId == request.auth.uid.
Check 1: Read your rules (2 minutes)
Open the Firebase console and look at every place that has rules:
- Firestore Database → Rules
- Realtime Database → Rules (if you use it)
- Storage → Rules (if you use it)
If you deploy with the Firebase CLI, the files named in firebase.json (usually firestore.rules, database.rules.json, storage.rules) are the source of truth, but check the console too. What is deployed is what counts.
Search for:
if truerequest.time <ornow <(test mode)request.auth != nullused alone, especially onmatch /{document=**}or on collections likeusers,orders,messages,payments".read": trueor".write": truein Realtime Database rules- a catch-all
match /{document=**}near the top. Firestore grants access if any matching rule allows it, so a broad allow at the top beats every careful rule below it.
Check 2: Ask your database as a stranger (3 minutes)
Get your project ID from the web config in your app. Then try to list a collection with no login at all. Use a collection you know exists, such as users:
curl -s -o /dev/null -w "%{http_code}\n" \
"https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/users?pageSize=1"
- 403 (
PERMISSION_DENIED): good, strangers can't list it. - 200: anyone can read this collection. Fix it today.
For the Realtime Database, use the databaseURL from your config:
curl -s -o /dev/null -w "%{http_code}\n" "https://YOUR-DB.firebaseio.com/.json?shallow=true"
401 is good. 200 means the whole database is readable (shallow=true only returns top-level keys, so you won't pull everything down).
For Storage, check whether strangers can list your files:
curl -s -o /dev/null -w "%{http_code}\n" \
"https://firebasestorage.googleapis.com/v0/b/YOUR_BUCKET/o?maxResults=1"
403 is good. 200 means anyone can list file names, and usually download the files.
Only run these against your own project.
Check 3: Ask again as a fresh, random user (5 minutes)
This is the check that catches rule #3. Create a throwaway account the same way an attacker would, through the public sign-up API, with the web API key from your own config:
curl -s "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=YOUR_WEB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"rules-test-1@example.com","password":"a-long-test-password-123","returnSecureToken":true}'
If this returns an idToken, anyone on the internet can create an account in your project, even if your app has no sign-up page. (If you only enabled Google sign-in this call fails, but anonymous sign-in or a Google account gives the same result.) Copy the idToken and repeat Check 2 with it:
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer ID_TOKEN" \
"https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/users?pageSize=1"
A brand-new user with no data should get 403 on collections that hold other people's data. A 200 here means any stranger who signs up can read your users. Do the same for orders, messages, profiles and whatever else your app stores. For the Realtime Database, add &auth=ID_TOKEN to the URL.
Delete the test user afterwards in Authentication → Users.
Check 4: Can a user make themselves admin? (3 minutes)
Many AI-built apps keep role: "user", plan: "free" or credits: 10 in the user's own document, and the rules let users update their own document. So a user can change it themselves:
// from any script using your app's public Firebase config, signed in as a normal user
await updateDoc(doc(db, "users", auth.currentUser.uid), { role: "admin", plan: "pro" });
If that works, your paywall and your admin panel are only decoration. In the Firebase console's Rules Playground (on the Rules tab) you can test this without code: simulate an update on /users/<a uid>, signed in as that same uid, with role: "admin" in the data.
Check 5: Storage rules and file size (2 minutes)
Storage test-mode rules have the same expiry trap. Check that files sit under a per-user path and that uploads are limited:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/{fileName} {
allow read: if request.auth != null && request.auth.uid == userId;
allow write: if request.auth != null && request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
}
}
Without a size and type limit, any signed-in user can upload gigabytes to your bucket, and you pay for the storage and download traffic.
The fix: owner-only rules you can paste
Here is a solid starting point for a typical app: a users collection keyed by uid, plus a collection of things that belong to one user (notes here, but the same goes for projects, orders or chats).
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() { return request.auth != null; }
function isAdmin() { return signedIn() && request.auth.token.admin == true; }
match /users/{userId} {
allow read: if signedIn() && request.auth.uid == userId;
allow create: if signedIn() && request.auth.uid == userId
&& !request.resource.data.keys().hasAny(['role', 'plan', 'credits']);
allow update: if signedIn() && request.auth.uid == userId
&& !request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['role', 'plan', 'credits']);
allow delete: if false;
}
match /notes/{noteId} {
allow read, delete: if signedIn() && resource.data.ownerId == request.auth.uid;
allow create: if signedIn() && request.resource.data.ownerId == request.auth.uid;
allow update: if signedIn() && resource.data.ownerId == request.auth.uid
&& request.resource.data.ownerId == resource.data.ownerId;
}
match /{document=**} {
allow read, write: if isAdmin();
}
}
}
What this does:
- No catch-all allow for normal users. Anything not listed is denied.
- Users can't edit protected fields.
role,planandcreditsare changed only by your server (Cloud Functions, or the Admin SDK in your backend). The Admin SDK skips rules, which is exactly why that code must never run in the browser. - Admins come from custom claims, set from your server with
setCustomUserClaims(uid, { admin: true }), not from a field users can write. - Ownership is checked on create and on update, so nobody can create a note "owned" by someone else or hand theirs over.
One thing that will surprise your AI tool: rules are not filters. With these rules, a query for all notes fails with a permission error even for a logged-in user. The query has to ask only for what the user may see: where("ownerId", "==", auth.currentUser.uid). When the app breaks after you tighten the rules, fix the query. Do not loosen the rule.
For the Realtime Database, the same idea looks like this:
{
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "auth != null && auth.uid === $uid"
}
}
}
}
Should strangers be able to create accounts at all? If your app is invite-only or internal, turn off public sign-up under Authentication → Settings → User actions → Enable create (sign-up). Hiding the sign-up page is not enough, because the API above still works.
Prompt to paste into your AI tool
Rewrite my Firestore, Realtime Database and Storage security rules. Requirements: no
if true, no expiry dates, no catch-allmatch /{document=**}that allows normal users, and neverrequest.auth != nullon its own. Every collection must check ownership (request.auth.uid == userIdorresource.data.ownerId == request.auth.uid) on read, create, update and delete. Users must not be able to changerole,plan,creditsor similar fields; those change only server-side. Admin access uses custom claims. Then update every query in the app to filter by the current user's uid so it matches the rules. List each collection, who can read it, and who can write it.
Then check the result yourself with Checks 2 and 3. Don't take the AI's word for it.
Make it stick: test your rules before you deploy
The Rules Playground is good for spot checks. For anything you will keep changing, write a small test against the Firestore emulator using @firebase/rules-unit-testing:
import { initializeTestEnvironment, assertFails, assertSucceeds } from "@firebase/rules-unit-testing";
import { doc, getDoc, setDoc } from "firebase/firestore";
import { readFileSync } from "node:fs";
const env = await initializeTestEnvironment({
projectId: "demo-rules-test",
firestore: { rules: readFileSync("firestore.rules", "utf8") },
});
await env.withSecurityRulesDisabled(async (ctx) => {
await setDoc(doc(ctx.firestore(), "notes/n1"), { ownerId: "alice", text: "hi" });
});
const alice = env.authenticatedContext("alice").firestore();
const bob = env.authenticatedContext("bob").firestore();
const stranger = env.unauthenticatedContext().firestore();
await assertSucceeds(getDoc(doc(alice, "notes/n1")));
await assertFails(getDoc(doc(bob, "notes/n1"))); // the "any signed-in user" test
await assertFails(getDoc(doc(stranger, "notes/n1")));
await assertFails(setDoc(doc(alice, "users/alice"), { role: "admin" }));
await env.cleanup();
console.log("rules OK");
Run it with firebase emulators:exec --only firestore "node rules.test.mjs". The bob line is the one that matters most: it fails the moment someone (or some AI) "simplifies" your rules back to request.auth != null.
What these rules don't cover
- App Check is not access control. It makes abuse from scripts harder, but it doesn't decide who may read which document. Rules do.
- Cloud Functions and your own API routes ignore Security Rules if they use the Admin SDK. Each one has to check who is calling (
request.authin a callable function, or verify the ID token yourself) and whether that user owns the data. - Leaked server keys. A service account JSON in your front-end code gives full admin access and skips every rule. See your API key is probably sitting in your JavaScript.
- Unrestricted browser API keys. The Firebase key is meant to be public, but if the same Google key also has Gemini or Maps turned on, restrict it by website and API in Google Cloud.
If you use Supabase instead, the same idea is called Row Level Security: see Supabase RLS in plain English. For everything else to check before launch, use the pre-launch security checklist.
Quick FAQ
Is it a problem that my Firebase API key is visible?
No, it is meant to be public. Your rules are what protect the data. Do restrict the key in Google Cloud if other paid APIs are turned on for it.
I got the "expiring in X days" email. Should I extend the date?
No. Extending it keeps your database open to everyone. Replace the rule with owner-based rules before the date.
My rules say request.auth != null. Is that safe?
Only if every signed-in user may see every document, which is rarely true. Anyone can sign up through the API, so treat it as nearly public.
Does Firebase warn me if my rules are insecure?
For fully open and test-mode rules, yes. For "any signed-in user" rules, there is no console warning. You have to check yourself.
Scan your app
Paste your app URL into the free Greenlight Scan. It finds the Firebase config and any leaked keys in your JavaScript. After you verify you own the app, it also tests your Realtime Database, Firestore collections and Storage bucket for access without login, and only counts: it never downloads your documents. The $19 Full Scan unlocks every finding with the exact fix and a prompt to paste into your AI tool. Then run Check 3 above for the signed-in case.
The Friday leak report
One short email a week: the newest ways AI-built apps leak data, and the one-line fix for each. No spam, unsubscribe any time.