VibeCheck

Guide · Firebase

Firebase security rules check

A Firebase app's web config, API key included, is public by design. What protects your data is Firebase Security Rules. Here is how to check them for Firestore, Realtime Database and Storage, and what good rules look like.

How Firebase access works

Your frontend talks directly to Firebase services using the project config (apiKey, projectId and so on). Anyone can copy that config from your site and make the same requests. Security Rules are evaluated by Firebase on every request and are the only thing that decides whether a read or write is allowed. The API key itself is not a secret; still, restrict it in Google Cloud Console to the APIs your app uses.

1. Look for test-mode rules

When you create a database in test mode, Firebase starts you with rules that allow everything, usually until a date about 30 days out. In Firestore it looks like:

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

Until that date, anyone can read and write every document. Often someone extends the date, or replaces the rule with allow read, write: if true; when the app "stops working". In the Realtime Database, the equivalent is:

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

Open the Firebase console, go to each product's Rules tab, and look for any of these patterns. The console also shows a warning banner when rules are public.

2. Test from the outside

For your own project, these requests show what an anonymous visitor gets.

Realtime Database. shallow=true returns only top-level keys, not the data:

curl 'https://YOUR-PROJECT-default-rtdb.firebaseio.com/.json?shallow=true'

A JSON object of keys means the root is readable. "Permission denied" is what you want. Older projects use YOUR-PROJECT.firebaseio.com; regional ones use a firebasedatabase.app domain. Use the databaseURL from your config.

Firestore, for a collection called users:

curl 'https://firestore.googleapis.com/v1/projects/YOUR-PROJECT/databases/(default)/documents/users?pageSize=1'

A 403 PERMISSION_DENIED is good. Documents in the response mean anyone can read that collection.

Cloud Storage:

curl 'https://firebasestorage.googleapis.com/v0/b/YOUR-BUCKET/o'

If this returns a list of items, anyone can list your files.

The Rules Playground in the console and the Firebase Local Emulator Suite let you test specific requests, signed in or not, without touching production.

3. Safe patterns

Firestore: each user owns their documents

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    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
                  || (request.auth != null && request.auth.uid == resource.data.authorId);
      allow create: if request.auth != null && request.resource.data.authorId == request.auth.uid;
      allow update, delete: if request.auth != null && request.auth.uid == resource.data.authorId;
    }
  }
}

Anything not matched by a rule is denied, so there is no need for a catch-all deny.

Realtime Database: per-user paths

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid === $uid",
        ".write": "auth != null && auth.uid === $uid"
      }
    }
  }
}

Storage: files under the user's folder

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

4. Common mistakes

"Signed in" is not "authorized"

allow read, write: if request.auth != null; lets every signed-in user read and change everyone's data. If anyone can create an account, or anonymous auth is enabled, this is barely better than public. Compare request.auth.uid with an owner field or path.

Realtime Database rules cascade

In the Realtime Database, a .read or .write that grants access at a parent path cannot be taken away by stricter rules on child paths. A permissive rule near the root opens everything below it.

Rules are not filters

Firestore will not silently drop documents a user cannot see; a query that could return forbidden documents fails. Developers sometimes "fix" the error by loosening the rule. Instead, make the query match the rule, for example by adding where("authorId", "==", uid).

Writes without validation

Allowing a user to write their own document is fine, but if that document has a role or isAdmin field, they can promote themselves. Use request.resource.data to check which fields change, or keep roles in custom claims set by a server (request.auth.token.admin == true).

Admin checks only in the app

Hiding an admin screen in JavaScript does not protect the data behind it. The rule must enforce the role.

Thinking App Check replaces rules

App Check makes it harder for scripts outside your app to call Firebase, which is useful against abuse. It does not decide which user can read which document; rules still do.

A short checklist