FIREBASE SECURITY RULES: 12 COMMON MISTAKES (AND FIXES)
Firebase Security Rules are the only gate between your data and the public internet. These 12 mistakes are the ones AI-generated apps ship most often — with the rule snippets that close each one.
TEST YOUR FIREBASE RULES NOW
Paste your app URL — we probe what anonymous and authenticated clients can actually read and write against your rules.
Test Mode Rules Expire After 30 Days
Firebase projects created in test mode use allow read, write: if true rules that expire after 30 days. AI-generated projects often forget to replace these with proper security rules, leaving databases vulnerable (while open) or inaccessible (after expiration). Treat test mode as a timer, not a security model.
Why Firebase rules matter for vibe-coded apps
Firestore and Realtime Database clients ship with project config and API keys in the browser by design. Rules are the authorization layer. If rules are open, every collection is an unauthenticated API. Tools like Firebase Studio, Cursor, and Bolt will happily generate working CRUD against an open project because “it works in the emulator and in the demo.” Production requires deny-by-default rules, ownership checks, and Storage parity.
Below: twelve mistakes with fix patterns. Snippets are illustrative — adapt field names and paths to your schema.
AI generators optimize for green demos. They rarely generate rules unit tests, rarely touch storage.rules, and often leave a recursive wildcard in place “temporarily.” Treat every new collection from a chat session as a rules PR, not just a client PR.
Mistake 1 — Open or expired test-mode rules
Broken
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true; // or request.time < timestamp.date(2026, 1, 1)
}
}
}
Fix — deny by default, open only known paths
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
// Explicit match blocks for each collection below
}
}
Ship the deny-all baseline first, then add per-collection rules. Never leave a catch-all if true.
Time-boxed open rules (request.time < timestamp.date(...)) are still open until the date — and many teams set the date years out “so we don’t break.” That is not a security control; it is a calendar.
Mistake 2 — Auth check missing or only on the client
AI UIs hide routes when user is null; Firestore still answers direct REST/SDK calls.
Broken
match /profiles/{userId} {
allow read, write: if true;
}
Fix
match /profiles/{userId} {
allow read: if request.auth != null;
allow write: if request.auth != null
&& request.auth.uid == userId;
}
For public profile reads with private writes:
match /profiles/{userId} {
allow get: if true; // or limit fields via public docs pattern
allow list: if false;
allow create, update: if request.auth != null
&& request.auth.uid == userId;
allow delete: if request.auth != null
&& request.auth.uid == userId;
}
Prefer get vs list carefully — open list is how scrapers dump entire collections.
Client-only guards (if (!user) return <Login />) are invisible to attackers with the SDK. Always assume the REST surface is the real API.
Mistake 3 — No ownership check (any signed-in user can touch any row)
Broken
match /orders/{orderId} {
allow read, write: if request.auth != null;
}
Fix
match /orders/{orderId} {
allow create: if request.auth != null
&& request.resource.data.ownerUid == request.auth.uid
&& !request.resource.data.keys().hasAny(['adminFlag', 'totalOverride']);
allow read, update, delete: if request.auth != null
&& resource.data.ownerUid == request.auth.uid;
}
Cross-user BOLA is the default failure mode of AI CRUD. See authorization patterns and BOLA in AI-generated CRUD.
Test with two real users: user B requests user A’s orderId by ID. If data returns, ownership failed regardless of how clean the UI feels.
Mistake 4 — No data shape validation
Auth-only rules still allow mass assignment: extra fields, type confusion, huge strings.
Fix sketch
function isValidPost(data) {
return data.keys().hasOnly(['title', 'body', 'ownerUid', 'createdAt'])
&& data.title is string
&& data.title.size() > 0
&& data.title.size() <= 120
&& data.body is string
&& data.body.size() <= 10000
&& data.ownerUid == request.auth.uid
&& data.createdAt == request.time;
}
match /posts/{postId} {
allow create: if request.auth != null && isValidPost(request.resource.data);
allow update: if request.auth != null
&& resource.data.ownerUid == request.auth.uid
&& isValidPost(request.resource.data);
allow read: if true;
allow delete: if request.auth != null
&& resource.data.ownerUid == request.auth.uid;
}
Reject unknown keys. Cap sizes. Bind ownership on create.
Mass assignment is how clients set price: 0 or role: 'admin' when those fields live on the same document as editable profile data. Split privileged fields into server-only documents when possible.
Mistake 5 — Clients control privileged fields
Roles, balances, isAdmin, verified must not be client-writable.
Broken
allow update: if request.auth != null
&& request.auth.uid == userId;
// client can set isAdmin: true
Fix
function profileUnchangedPrivileged() {
return !request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['isAdmin', 'role', 'balance', 'stripeCustomerId']);
}
match /users/{userId} {
allow update: if request.auth != null
&& request.auth.uid == userId
&& profileUnchangedPrivileged();
}
Set privileged fields only from Admin SDK / Cloud Functions with service credentials.
Custom claims on the ID token are the right place for admin flags used in rules (request.auth.token.admin == true). Claims are set server-side; clients cannot forge them without the service account.
Mistake 6 — Subcollections unprotected
Rules do not inherit. Parent lock ≠ child lock.
Broken — only /users/{userId} secured; /users/{userId}/documents/{docId} open.
Fix
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
match /documents/{docId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
match /privateNotes/{noteId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
}
Enumerate every subcollection your client SDK uses. AI generators love nested paths and forget rules for them.
A practical audit: grep the client for .collection( and doc( paths, build a set of path templates, and ensure each template has a match block. If the client can reach it, rules must mention it.
Mistake 7 — Storage rules left open
Firestore locked; Cloud Storage still:
// BAD default-ish
allow read, write: if true;
Fix pattern
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /userUploads/{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/.*');
}
}
}
Validate size and content type. Do not store secrets in downloadable objects. Public marketing images can use a dedicated public path with read-only rules — not the same bucket path as user PII.
Content-type checks are necessary but not sufficient (polyglots exist). Still enforce them; add virus scanning or server-side re-encoding for high-risk uploads.
Mistake 8 — Roles without custom claims (or claims ignored)
Checking a role field in Firestore that the user can edit is theater. Use custom claims set by Admin SDK:
// Rules
function isAdmin() {
return request.auth != null
&& request.auth.token.admin == true;
}
match /moderation/{id} {
allow read, write: if isAdmin();
}
Claims live on the ID token; rotate/revoke sessions when roles change. Document claim names in your runbook.
When demoting an admin, revoke refresh tokens so old JWTs with admin: true die early. Rules only see the token presented on the request.
Mistake 9 — Client-controlled timestamps and audit fields
Broken — trust createdAt from the client for ordering or fraud windows.
Fix
allow create: if request.auth != null
&& request.resource.data.createdAt == request.time
&& request.resource.data.ownerUid == request.auth.uid;
allow update: if request.auth != null
&& request.auth.uid == resource.data.ownerUid
&& request.resource.data.createdAt == resource.data.createdAt;
Immutable audit fields stay equal to resource.data on update.
The same pattern applies to createdBy, invoice totals, and referral codes — anything the client should not author.
Mistake 10 — Expensive unbounded queries allowed by rules
Rules are not a full WAF, but you can deny dangerous patterns and force queries to include constraints the rules can prove (e.g. ownerUid == auth.uid). Design data so that every client query is one the rules can authorize without opening full collection scans to the world.
For heavy abuse (scraping, cost), add App Check, rate limits at Cloud Functions/API Gateway, and monitoring — rules alone will not stop authenticated bulk export if list is allowed broadly. See API abuse protection.
Example design pressure: if every post is world-listable, scrapers will list. Prefer public get by ID, curated public collections, or Cloud Functions for search.
Mistake 11 — Never tested in the emulator
Deploying rules “from the AI chat” without tests is how open list rules reach production.
Emulator unit test sketch (Node):
const {
assertFails,
assertSucceeds,
initializeTestEnvironment,
} = require("@firebase/rules-unit-testing");
const fs = require("fs");
let testEnv;
before(async () => {
testEnv = await initializeTestEnvironment({
projectId: "demo-rules",
firestore: {
rules: fs.readFileSync("firestore.rules", "utf8"),
},
});
});
it("owner can read own order; stranger cannot", async () => {
await testEnv.withSecurityRulesDisabled(async (ctx) => {
await ctx.firestore().doc("orders/o1").set({ ownerUid: "userA", total: 10 });
});
const owner = testEnv.authenticatedContext("userA");
const stranger = testEnv.authenticatedContext("userB");
await assertSucceeds(owner.firestore().doc("orders/o1").get());
await assertFails(stranger.firestore().doc("orders/o1").get());
});
it("unauthenticated cannot write profiles", async () => {
const anon = testEnv.unauthenticatedContext();
await assertFails(
anon.firestore().doc("profiles/userA").set({ name: "x" })
);
});
Cover: anon read/write, owner happy path, cross-user IDOR, privileged field writes, Storage uploads over size limit.
Wire the suite into CI so a rules regression fails the PR the same way a unit test does.
Mistake 12 — No monitoring or documentation
Enable rules metrics / monitoring in Firebase console for denials spikes (recon) and unexpected allow spikes after a deploy. Comment complex functions in the rules file so the next AI edit does not “simplify” them back to if true.
// OWNERSHIP: orders.ownerUid must equal auth.uid for all client paths.
// Privileged refunds only via Cloud Functions (Admin SDK).
function isOwner() {
return request.auth != null
&& resource.data.ownerUid == request.auth.uid;
}
When denials spike after a legitimate feature launch, you have a client bug. When allows spike after a rules deploy, you may have opened a path — treat that as an incident until explained.
Realtime Database notes (if you still use it)
Realtime Database rules cascade. A root .read: true poisons every child. Mirror the twelve mistakes with JSON rules:
{
"rules": {
".read": false,
".write": false,
"orders": {
"$orderId": {
".read": "auth != null && data.child('ownerUid').val() === auth.uid",
".write": "auth != null && (!data.exists() && newData.child('ownerUid').val() === auth.uid || data.child('ownerUid').val() === auth.uid)"
}
}
}
}
Audit from the root down. AI chat demos love open root rules.
Cloud Functions vs rules
Rules never apply to Admin SDK access. Any callable that accepts a path or collection name from the client is a full bypass:
// Reject this pattern
admin.firestore().doc(data.path).get();
// Prefer fixed paths from auth context
admin.firestore().doc(`users/${context.auth.uid}`).get();
Keep privileged writes in Functions; keep client-readable shapes narrow; re-validate auth and authorization inside every Function even if the client already “checked.”
Firebase Security Rules Implementation Checklist
Use this as a ship gate for every collection and bucket:
- Secure Firestore collections — no
if truecatch-alls; deny default. - Validate authentication —
request.auth != nullwhere required. - Ownership — uid matches document owner for user data.
- Validate types and structure —
request.resource.dataschema checks. - Field-level protection — block admin/role/balance client writes.
- Subcollections — explicit
matchfor every path. - Storage rules — auth, size, content type, path ownership.
- Custom claims for admin/moderator — not editable Firestore fields.
- Timestamps —
request.time; immutable created fields. - Abuse / cost — App Check, function-level rate limits, careful
list. - Emulator tests — owner, stranger, anon, bad payload.
- Monitor + document — denial metrics and comments on business logic.
Common Firebase Security Issues (summary)
| Issue | Symptom | Fix |
|---|---|---|
| Default permissive rules | World read/write | Deny default + per-path rules |
| Missing subcollection rules | Nested data open | Explicit nested match |
| No data validation | Mass assignment | hasOnly + types + sizes |
| Storage forgotten | Files public | Separate Storage rules file |
| Auth without ownership | BOLA | Compare auth.uid to owner field |
| Client roles | Privilege escalation | Custom claims + Admin SDK |
| Unbounded list | Full collection scrape | Prefer get; constrain queries |
| Untested rules | Silent production holes | Emulator unit tests in CI |
Related Resources
- Firebase Security Guide — Product-oriented hardening
- Database Security Best Practices — Universal DB principles
- Authentication Implementation — Secure auth patterns
- Authorization Patterns — RBAC and access control
- Firebase Scanner — Probe live Firebase exposure
- Supabase RLS Guide — Parallel model if you use Supabase
- Is Firebase Safe? — Platform-level safety review
Rules unit testing in CI
# GitHub Actions sketch
- run: npm ci
- run: npm test -- rules.test.js
env:
FIRESTORE_EMULATOR_HOST: 127.0.0.1:8080
Fail the build if owner/non-owner/unauth matrices regress. Console Playground clicks are not CI.
Custom claims rollout
Mint claims with Admin SDK; never from client writes. Document claim names (admin, reseller) and which paths require them. Expire/refresh tokens after claim changes.
Migrating off open rules
- Deploy rules that deny by default in a staging project.
- Fix the app against permission errors.
- Ship rules to prod during low traffic.
- Watch for elevated permission-denied metrics.
- Keep the dual-user probe green.
Multi-tenant membership checks with get()
When documents are shared across a team, ownership is not auth.uid == userId. Model membership carefully:
function isMember(orgId) {
return request.auth != null
&& get(/databases/$(database)/documents/orgs/$(orgId)/members/$(request.auth.uid)).data.active == true;
}
match /orgs/{orgId}/projects/{projectId} {
allow read: if isMember(orgId);
allow write: if isMember(orgId)
&& request.resource.data.keys().hasOnly(['name', 'updatedAt', 'ownerUid'])
&& request.resource.data.ownerUid == resource.data.ownerUid;
}
get() costs a read—cache membership on the token via custom claims when the org set is small and stable. Document claim refresh after invite/revoke.
Composite indexes and rule-query alignment
Rules that require resource.data.ownerUid == request.auth.uid force clients to filter by ownerUid. AI clients often query the whole collection and filter in memory—those queries fail closed (good) or developers “fix” by opening list (bad). Align:
- Client queries always include the equality the rules need.
- Composite indexes exist for those filters.
- Integration tests assert permission-denied on unfiltered list attempts.
Realtime listeners as scrape amplifiers
Open list + onSnapshot is a live firehose. Prefer:
getfor public docs by known ID- Authenticated queries with ownership filters
- Server-generated feeds via Cloud Functions for complex fan-out
Monitor read spikes after shipping a new listener path.
Storage + Firestore dual-write consistency
Apps store photoURL in Firestore while the blob lives in Storage. Rules must match:
| Path | Firestore | Storage |
|---|---|---|
| Avatar | owner write profile fields only | userUploads/{uid}/** owner write, size/type caps |
| Shared deck | member read | member read on orgs/{orgId}/files/** |
| Invoices | owner/admin | never public; signed URLs from Functions |
AI generators secure one side and leave the other open. Audit both files in the same PR.
App Check + rules (defense in depth)
App Check reduces non-app clients; rules still authorize humans and stolen tokens. Enable App Check enforcement in stages: monitor mode → enforce on Storage → enforce on Firestore. Document break-glass for CI emulator tests (debug tokens, never production).
Emulator matrix you should not skip
const cases = [
["anon", null, "read", "orders/o1", false],
["owner", "userA", "read", "orders/o1", true],
["stranger", "userB", "read", "orders/o1", false],
["owner", "userA", "update-adminFlag", "users/userA", false],
];
Run on every PR that touches firestore.rules or storage.rules. Console Playground clicks are not CI.
Rules review when AI adds a collection
PR template checkbox:
- New
matchpath for every client collection path - Subcollections nested under correct parent match
-
hasOnlyfield allowlists on writes - Privileged fields blocked or claims-gated
- Storage path if uploads involved
- Emulator tests for owner/stranger/anon
- No catch-all
if trueleft from scaffolding
Cost attacks via open rules
Open write enables document spam that burns write quotas and triggers cascading listeners. Open read enables full collection export. Pair rules with App Check, rate limits on callable Functions, and billing alerts. Rules authorize; they are not a full WAF—see API abuse protection.
Production incident: reopen after “hotfix”
Composite pattern: support asks for a temporary public read on tickets during an outage; AI chat applies allow read: if true; outage ends; rule remains. Detection: rules denylist CI diff, weekly drift check against the last known-good rules file, and metrics on allow rates. Require ticket ID in rules comments for any temporary exception with an expiry date string the tests parse.
Verification cadence for Firebase rules
After every AI-assisted change that touches collections, Storage paths, or claims: run emulator owner/stranger/anon matrices, dual-user probe on staging, and a live Firebase Scanner pass. Critical open paths block release. Record the last green dual-user date in the release ticket.
Test Your Firebase Security Rules
Reading rules is necessary; exercising them is decisive. VibeEval tests what anonymous and authenticated clients can actually read and write against your deployed project — open collections, missing ownership checks, and Storage gaps — so you fix what is proven, not what the AI claimed was locked down.
COMMON QUESTIONS
VERIFY RULES AGAINST A LIVE APP
Reading rules is not enough. We exercise your deployed Firebase project for open collections, missing ownership checks, and Storage gaps.
14-day free trial · No credit card · Cancel anytime