Rustine

Firebase Security Rules That Don't Leak Your Data

Firebase Security Rules are the only thing standing between your database and anyone who finds your app's config. If you built your app with an AI assistant, there's a good chance it shipped the default "test mode" rules without telling you — which means anyone can read or delete everything. This guide gives you copy-paste rules for Firestore, Realtime Database and Cloud Storage, plus a prompt to hand your coding agent so it locks the data down before launch.

Firebase Security Rules That Don't Leak Your Data
Contents
  1. What are Firebase Security Rules?
  2. The leak most vibe-coded apps ship
  3. Rules are not a filter
  4. Firestore rules that keep data private
  5. Realtime Database rules: watch the cascade
  6. Cloud Storage rules for uploads
  7. What bypasses your rules entirely
  8. How to test rules before you deploy
  9. The prompt to paste into your agent
  10. Before you launch
  11. Frequently asked questions
  12. Are Firebase Security Rules enough to secure my app?
  13. What happens if my rules deny a legitimate query?
  14. How do I know if my data is already exposed?
  15. Can I let users read a document but hide one field?
  16. Do rules cost anything to run?
  17. Sources

Editorial illustration of Firebase security rules acting as a gate between a developer and database documents

What are Firebase Security Rules?

Security Rules are the access-control layer that sits in front of three Firebase products: Cloud Firestore, Realtime Database and Cloud Storage. They decide who can read or write which data, and they are enforced on Google's servers — not in your JavaScript.

That last point matters. Your frontend code can pretend to hide the "admin settings" page, but if the rules allow it, a user can open the browser's network tab and query the database directly. Rules are the real lock. The client SDK exposes your project's config to anyone who loads the page, so rules are the only reason that config can't be used to empty your database.

The syntax looks like a small expression language. A rule is a match on a path, plus an allow statement that is either true or false:

javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Read that as: a user may read and write the document /users/{userId} only when they are logged in and their ID matches the document's ID.

The leak most vibe-coded apps ship

When you create a Firebase project and let a tool like Cursor, Claude Code, Lovable or Bolt wire it up, the assistant often enables "test mode" so the demo works instantly. Test mode rules look like this:

javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.time < timestamp.date(2026, 11, 21);
    }
  }
}

That rule allows everyone to read and write everything until the date in the condition. The date buys you a grace period, not security. The instant it passes, the rules lock — but for the weeks before, your data is a public shared drive. Worse, some generated apps use allow read, write: if true;, which never expires at all.

The second-most-common mistake looks safer but isn't:

javascript
allow read, write: if request.auth != null;

This lets any logged-in user read and modify any other user's data. It stops anonymous visitors but does nothing to stop a user who signs up, then reads your whole users collection. This is the same class of bug that shows up in vibe-coded apps: the assistant writes the happy path, not the adversary path.

Rules are not a filter

This is the single most important concept to internalise, and it's the one people get wrong in production.

Security Rules do not filter data. They don't return only the documents a user is allowed to see. If a query could match even one document the user can't read, the entire query is denied.

Illustration showing a database query meeting document cards, some blocked and pushed aside in orange

Suppose you write a rule that says "users can read any post where published is true", and your app runs a query for the latest 20 posts without filtering on published. If the 20 newest posts include one that is still a draft, Firebase denies the whole query — it won't quietly skip the draft. The fix is to make the rule and the query agree: add the same condition to both.

This is different from how a lot of developers assume it works, and it's a common source of "works in dev, breaks in prod" bugs. For a deeper look at the same mental model in a different backend, see how Supabase RLS handles it — the principle is identical, just a different syntax.

Firestore rules that keep data private

Start from locked, then open specific doors. The correct default is to deny everything and whitelist what your app actually needs:

javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
    match /users/{userId} {
      allow read, update, delete: if request.auth != null && request.auth.uid == userId;
      allow create: if request.auth != null && request.auth.uid == userId;
    }
    match /posts/{postId} {
      allow read: if resource.data.published == true;
      allow create: if request.auth != null && request.resource.data.authorId == request.auth.uid;
      allow update, delete: if request.auth != null && resource.data.authorId == request.auth.uid;
    }
  }
}

A few things to note in that block:

  • request.auth.uid is the logged-in user's ID; request.auth is null when the request is anonymous.
  • resource.data is the document as it currently exists (for a read, update or delete). request.resource.data is the document as it would be after the write (for a create or update). Use the right one: checking resource.data.authorId on a create reads nothing, because the document doesn't exist yet.
  • Field-level control is real but easy to overrate. You can allow reading a document while denying certain fields, but the rules are checked per-document, not per-field on reads — field restrictions on reads only work through get()/exists() calls or separate public/private subcollections. For most solo-founder apps, the cleaner fix is to store sensitive fields in a separate private document rather than fight field-level rules.

If you need an admin role, use custom claims rather than a hardcoded list of IDs. You set a claim like { role: "admin" } from a secure environment, and read it in rules as request.auth.token.role == 'admin'.

Realtime Database rules: watch the cascade

Realtime Database uses a different, JSON-shaped rule language — and it has one trap that Firestore doesn't.

In Realtime Database, rules cascade. If you grant read access at the root, that access reaches every child path, and a deeper rule can only add more access — it cannot take access away. The leak pattern is exactly this:

json
{
  "rules": {
    ".read": true,
    ".write": true
  }
}

That's the "open to the world" config, and it's also what a quick-start often leaves behind. The fix is the mirror of Firestore: deny at the root, then grant access only at the specific paths you control:

json
{
  "rules": {
    ".read": false,
    ".write": false,
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid"
      }
    },
    "posts": {
      ".read": true,
      "$postId": {
        ".write": "auth != null && newData.child('authorId').val() === auth.uid"
      }
    }
  }
}

The $uid and $postId syntax is a wildcard that captures a path segment, so you can compare it against the logged-in user's ID. Keep the root locked and treat every .read: true you add as a decision you have to justify.

Cloud Storage rules for uploads

Cloud Storage has its own rule surface, and it's where unauthenticated writes usually cause the most damage — anyone who can upload can fill your bucket and run up your bill.

javascript
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if false;
    }
    match /avatars/{userId}/{fileName} {
      allow read: if true;
      allow write: if request.auth != null
                   && request.auth.uid == userId
                   && request.resource.size < 5 * 1024 * 1024
                   && request.resource.contentType.matches('image/.*');
    }
  }
}

The request.resource.size and request.resource.contentType checks are your defence against someone storing a 2 GB file or an .html payload in a folder you thought was only for avatars. Set a size cap and an allowlist of content types on every public upload path.

What bypasses your rules entirely

Rules protect the client SDK path. They do not protect the Firebase Admin SDK, which uses a service account with full, unconditional access.

That's a feature — your own backend needs to run migrations and background jobs. But it's also a trap: if a service-account credential leaks, rules won't slow it down, because the Admin SDK never consults rules at all. Treat your service account JSON the way you'd treat a root password, and never bundle it into a client bundle.

This is the Firebase version of a mistake we've written about before: the Supabase service role key has exactly the same behaviour — it skips every policy you wrote. When you store a credential that bypasses your access control, the credential is your access control.

Illustration of an admin service account entering through a side door, bypassing the main rules gate

How to test rules before you deploy

You don't have to ship rules and hope. Two tools will catch most mistakes for free:

  • The Firebase Emulator Suite runs Firestore, Realtime Database and Storage locally, so you can write a small test script that signs in as a few fake users and asserts what each can read. This is the one worth setting up if you're about to launch.
  • The Rules Playground, in the Firebase console, lets you simulate a single request — pick a path, pick an auth state, and see whether the rule allows or denies it.

The most useful test to run first: an unauthenticated request against /users. If it returns anything other than "denied", you have work to do.

The prompt to paste into your agent

This is the Rustine signature: don't stop at the diagnosis, ship the patch. Hand this to Cursor, Claude Code or Lovable and let it fix the rules for you:

code
I'm building a [describe your app] on Firebase, and my security rules are currently too open. Please:

1. Read my rules files: firestore.rules, database.rules.json and storage.rules.
2. Replace any rule using `if true` or `if request.auth != null` on a whole collection with per-user rules, using `request.auth.uid == userId` for documents the user owns.
3. Add a locked default at the top of each file — `match /{document=**} { allow read, write: if false; }` for Firestore, `".read": false, ".write": false` for Realtime Database — and then open only the specific paths my app reads publicly.
4. For every path you open to public reads, list it back to me with the reason, so I can approve each one before you deploy.
5. Add an Emulator Suite test that signs in as two different users and verifies neither can read the other's /users document, then run it and show me the output.

Then check that an unauthenticated request to /users is denied, that Realtime Database has no `.read: true` at the root, and that no service account file is referenced anywhere in the client bundle.

Before you launch

Rules are the biggest lever, but they're one item on a longer list. A quick pass should also cover API keys you may have shipped in the client, security headers, and the Supabase-specific equivalents if you're on the other stack. Two places to continue: the vibe coding security checklist for the full pre-launch pass, and the Supabase security checklist if you went with Postgres instead.

Frequently asked questions

Are Firebase Security Rules enough to secure my app?

They're the access-control layer, not the whole security story. Rules prevent direct reads and writes through the client SDK, but they don't protect you from leaked service-account credentials, overly permissive server code, or a client that ships secrets. Treat them as necessary, not sufficient.

What happens if my rules deny a legitimate query?

The query fails completely — Firebase doesn't return the allowed subset. You fix it by making the rule and the query enforce the same condition, for example filtering on published == true in both places.

How do I know if my data is already exposed?

Check the Rules tab in the Firebase console for if true, request.auth != null on whole collections, or a test-mode date in the past. Then run an unauthenticated read against /users in the Rules Playground. If it's denied, you're not wide open.

Can I let users read a document but hide one field?

Not cleanly on a per-field read basis. Rules are evaluated per document, so the practical approach is to move sensitive fields into a separate, private document or subcollection and keep the public document lean.

Do rules cost anything to run?

Rule evaluation is included in the normal request path — you pay for the underlying document reads, and get()/exists() calls in rules count as reads against your quota, so use them sparingly.


If you're not sure whether your generated app left a door open, scan it the way a stranger would: check your site with Rustine and you'll see exactly what leaks, with the proof and the fix to paste back into your agent.

Sources

Is your app affected? Find out in a minute.Scan my site