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:

  1. Secure Firestore collections — no if true catch-alls; deny default.
  2. Validate authenticationrequest.auth != null where required.
  3. Ownership — uid matches document owner for user data.
  4. Validate types and structurerequest.resource.data schema checks.
  5. Field-level protection — block admin/role/balance client writes.
  6. Subcollections — explicit match for every path.
  7. Storage rules — auth, size, content type, path ownership.
  8. Custom claims for admin/moderator — not editable Firestore fields.
  9. Timestampsrequest.time; immutable created fields.
  10. Abuse / cost — App Check, function-level rate limits, careful list.
  11. Emulator tests — owner, stranger, anon, bad payload.
  12. 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

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

  1. Deploy rules that deny by default in a staging project.
  2. Fix the app against permission errors.
  3. Ship rules to prod during low traffic.
  4. Watch for elevated permission-denied metrics.
  5. 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:

  1. Client queries always include the equality the rules need.
  2. Composite indexes exist for those filters.
  3. Integration tests assert permission-denied on unfiltered list attempts.

Realtime listeners as scrape amplifiers

Open list + onSnapshot is a live firehose. Prefer:

  • get for 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 match path for every client collection path
  • Subcollections nested under correct parent match
  • hasOnly field allowlists on writes
  • Privileged fields blocked or claims-gated
  • Storage path if uploads involved
  • Emulator tests for owner/stranger/anon
  • No catch-all if true left 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

01
What is the most common Firebase rules mistake in AI-generated apps?
Leaving test-mode rules (allow read, write: if true) or open rules that never got replaced. AI scaffolds often ship open access so the demo works, then the project goes to production without a rules rewrite.
Q&A
02
Do Firestore security rules apply to subcollections automatically?
No. Rules do not cascade. A locked-down /users/{userId} document does not protect /users/{userId}/orders/{orderId}. Write explicit match blocks for every path clients can hit.
Q&A
03
Is request.auth != null enough for user data?
No. That only proves someone is signed in. User-owned data needs ownership checks such as request.auth.uid == userId, plus validation that clients cannot set admin or role fields.
Q&A
04
How do I test Firebase security rules safely?
Use the Firebase Emulator Suite with unit tests (@firebase/rules-unit-testing) for authenticated, unauthenticated, and cross-user cases. Never use production data for red-team experiments.
Q&A
05
Are Storage rules separate from Firestore rules?
Yes. Cloud Storage has its own rules file. Securing Firestore alone leaves uploaded files world-readable if Storage stays open. Mirror auth and ownership patterns in Storage rules.
Q&A
06
Do get() and exists() in rules cost money?
Yes — cross-document gets in rules are billable reads. Design data so ownership lives on the document when possible; use get() sparingly for membership checks.
Q&A
07
Can rules replace App Check?
No. Rules authorize; App Check reduces automated abuse from non-app clients. Use both in production.
Q&A
08
Should public marketing content use open read rules?
Yes when intentional — prefer get without list, or a dedicated public collection. Never open the same path that holds PII.
Q&A

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

RUN FIREBASE SCAN