Security Guides·20 min read

Securing Firebase in Production: Complete Security Checklist

A comprehensive guide to securing your Firebase application. Learn how to lock down Firestore rules, RTDB permissions, Cloud Storage, Cloud Functions, App Check, Data Connect, and Genkit.

Victor

Victor

Securing Firebase in Production: Complete Security Checklist

Firebase powers millions of applications, from small side projects to enterprise-scale platforms. Its developer-friendly approach—client-side SDKs, real-time sync, and managed infrastructure—makes it incredibly productive to build with.

But that productivity comes with security responsibility. The numbers are staggering:

  • 2024: 916 Firebase websites exposed 125 million user records, including 19 million plaintext passwords
  • 2025: Security researchers found 150+ popular apps (many with 100M+ downloads) leaking sensitive data through Firebase misconfigurations
  • Exposed data included payment details, private messages, AWS tokens, and cleartext credentials

Most of these breaches were completely preventable. Firebase has no database security enabled by default—it's entirely the developer's responsibility to configure security rules before storing real data.

This guide covers every aspect of Firebase security you need to get right before going to production.

1. Firestore Security Rules

Risk Level: Critical

Firestore security rules are your primary defense against unauthorized data access. They run on Google's servers and can't be bypassed from the client.

The Problem: Open Rules

The most dangerous configuration is the default "test mode" rules that many developers forget to change:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true; // DANGER: Anyone can read/write everything!
    }
  }
}

The Fix: Proper Authentication and Authorization

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Users can only access their own profile
    match /users/{userId} {
      allow read, write: if request.auth != null
        && request.auth.uid == userId;
    }

    // Posts: anyone can read, only author can write
    match /posts/{postId} {
      allow read: if 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;
    }

    // Admin-only collection
    match /admin/{document=**} {
      allow read, write: if request.auth != null
        && request.auth.token.admin == true;
    }
  }
}

Common Mistakes to Avoid

1. Using request.auth != null alone:

// BAD: Any authenticated user can access ANY user's data
match /users/{userId} {
  allow read, write: if request.auth != null;
}

// GOOD: Users can only access their own data
match /users/{userId} {
  allow read, write: if request.auth.uid == userId;
}

2. Forgetting to validate data on writes:

// BAD: User can set themselves as admin
match /users/{userId} {
  allow write: if request.auth.uid == userId;
}

// GOOD: Validate fields that shouldn't be user-controlled
match /users/{userId} {
  allow write: if request.auth.uid == userId
    && !request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['isAdmin', 'role', 'subscriptionTier']);
}

3. Not restricting collection listing:

// Consider if users should be able to list ALL documents
match /users/{userId} {
  // This allows listing all users if they know the path
  allow list: if request.auth != null;

  // Better: Only allow get (single document)
  allow get: if request.auth.uid == userId;
}

2. Realtime Database Rules

Risk Level: Critical

RTDB uses a JSON-based rules system. The same principles apply, but the syntax differs.

The Problem: Public Database

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

This exposes your entire database to anyone who knows the project ID.

The Fix: Scoped Access

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

RTDB-Specific Concerns

1. Path traversal via parent rules:

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "$uid === auth.uid"
      }
    },
    // DANGER: This allows reading ALL users at once!
    ".read": "auth !== null"
  }
}

Rules cascade—a read permission at a parent level grants access to all children.

2. Validate data structure:

{
  "rules": {
    "messages": {
      "$messageId": {
        ".validate": "newData.hasChildren(['text', 'timestamp', 'userId'])",
        "text": {
          ".validate": "newData.isString() && newData.val().length < 500"
        },
        "timestamp": {
          ".validate": "newData.val() === now"
        },
        "userId": {
          ".validate": "newData.val() === auth.uid"
        }
      }
    }
  }
}

3. Cloud Storage Rules

Risk Level: High

Cloud Storage rules protect files from unauthorized access. Without them, anyone can read (or write) your files.

The Problem: Public Bucket

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true; // Public bucket!
    }
  }
}

The Fix: User-Scoped Access

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    // User profile pictures - user can read/write their own
    match /users/{userId}/profile/{fileName} {
      allow read: if true; // Public profile pictures
      allow write: if request.auth.uid == userId
        && request.resource.size < 5 * 1024 * 1024 // 5MB limit
        && request.resource.contentType.matches('image/.*');
    }

    // Private user files
    match /users/{userId}/private/{allPaths=**} {
      allow read, write: if request.auth.uid == userId;
    }

    // Shared files - check custom claims or database
    match /shared/{fileId} {
      allow read: if request.auth != null;
      allow write: if request.auth.token.admin == true;
    }
  }
}

Storage-Specific Validations

1. File type validation:

match /uploads/{userId}/{fileName} {
  allow write: if request.auth.uid == userId
    && request.resource.contentType in ['image/png', 'image/jpeg', 'application/pdf'];
}

2. File size limits:

match /uploads/{userId}/{fileName} {
  allow write: if request.auth.uid == userId
    && request.resource.size < 10 * 1024 * 1024; // 10MB
}

3. Prevent dangerous files:

// Never allow executable uploads
match /uploads/{allPaths=**} {
  allow write: if !request.resource.contentType.matches('application/(x-executable|x-msdownload|x-sh).*');
}

4. Cloud Functions Security

Risk Level: High

Cloud Functions can be HTTP-callable (public endpoints) or background triggers. HTTP functions need careful authentication.

The Problem: Unauthenticated Functions

// DANGEROUS: No authentication check
export const deleteUser = functions.https.onRequest(async (req, res) => {
  const userId = req.body.userId;
  await admin.auth().deleteUser(userId);
  res.send({ success: true });
});

The Fix: Verify Authentication

Option 1: Firebase Callable Functions (Recommended)

export const deleteUserData = functions.https.onCall(async (data, context) => {
  // Authentication is automatically verified
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Must be logged in');
  }

  // Authorization: user can only delete their own data
  if (data.userId !== context.auth.uid) {
    throw new functions.https.HttpsError('permission-denied', 'Cannot delete other users');
  }

  // Proceed with deletion
  await admin.firestore().collection('users').doc(data.userId).delete();
  return { success: true };
});

Option 2: HTTP Functions with Manual Verification

export const adminEndpoint = functions.https.onRequest(async (req, res) => {
  // CORS
  res.set('Access-Control-Allow-Origin', 'https://yourapp.com');

  // Verify Firebase ID token
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    res.status(401).send({ error: 'Unauthorized' });
    return;
  }

  const token = authHeader.split('Bearer ')[1];
  try {
    const decodedToken = await admin.auth().verifyIdToken(token);

    // Check admin claim
    if (!decodedToken.admin) {
      res.status(403).send({ error: 'Forbidden' });
      return;
    }

    // Process request
    res.send({ success: true });
  } catch (error) {
    res.status(401).send({ error: 'Invalid token' });
  }
});

Function Security Best Practices

1. Rate limiting:

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

export const api = functions.https.onRequest(async (req, res) => {
  await new Promise((resolve, reject) => {
    limiter(req, res, (err) => {
      if (err) reject(err);
      else resolve(null);
    });
  });

  // Handle request
});

2. Input validation:

import { z } from 'zod';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1).max(10000),
  tags: z.array(z.string()).max(10).optional(),
});

export const createPost = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Must be logged in');
  }

  // Validate input
  const parsed = CreatePostSchema.safeParse(data);
  if (!parsed.success) {
    throw new functions.https.HttpsError('invalid-argument', parsed.error.message);
  }

  // Safe to use parsed.data
});

5. Firebase App Check

Risk Level: High

App Check is a critical security layer that protects your Firebase backend from abuse. While Authentication verifies who is making a request, App Check verifies what is making the request—ensuring calls come from your legitimate app, not scripts or modified clients.

The Problem: Anyone Can Call Your APIs

Even with authentication, attackers can:

  • Reverse-engineer your app and extract the Firebase config
  • Call your APIs from scripts, bots, or modified apps
  • Exhaust your quotas or abuse your backend
  • Bypass client-side validation entirely

The Fix: Enable App Check

1. Configure attestation providers in Firebase Console:

Each platform uses a different attestation method:

  • Web: reCAPTCHA Enterprise (recommended) or reCAPTCHA v3
  • Android: Play Integrity API
  • iOS: App Attest (iOS 14+) or DeviceCheck

2. Initialize App Check in your app:

// Web - using reCAPTCHA Enterprise (recommended)
import { initializeApp } from 'firebase/app';
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';

const app = initializeApp(firebaseConfig);

const appCheck = initializeAppCheck(app, {
  provider: new ReCaptchaEnterpriseProvider('your-recaptcha-site-key'),
  isTokenAutoRefreshEnabled: true
});

3. Enforce App Check in Cloud Functions:

import { onCall } from 'firebase-functions/v2/https';

export const sensitiveOperation = onCall(
  {
    enforceAppCheck: true, // Reject requests without valid App Check token
  },
  async (request) => {
    // This code only runs if App Check verified the request
    // request.app contains the App Check token info
    return { success: true };
  }
);

App Check Best Practices

1. Gradual enforcement rollout:

Don't enable enforcement immediately—monitor first:

  • Deploy the updated client with App Check initialized
  • Monitor the Firebase Console for App Check metrics
  • Once most traffic has valid tokens, enable enforcement

2. Handle debug environments:

// ONLY in development builds
if (process.env.NODE_ENV === 'development') {
  // @ts-ignore
  self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}

const appCheck = initializeAppCheck(app, {
  provider: new ReCaptchaEnterpriseProvider('your-site-key'),
  isTokenAutoRefreshEnabled: true
});

Warning: Never set FIREBASE_APPCHECK_DEBUG_TOKEN in production. Debug tokens bypass attestation entirely.

3. Quota considerations:

  • Play Integrity: 10,000 calls/day (free tier)
  • reCAPTCHA Enterprise: 10,000 assessments/month (free), then $1 per 1,000

For high-traffic apps, consider caching strategies and longer token refresh intervals.

4. Verify enforcement is working:

Test that unauthenticated requests fail:

# This should return an error after enforcement is enabled
curl -X POST https://your-region-your-project.cloudfunctions.net/sensitiveOperation \
  -H "Content-Type: application/json" \
  -d '{"data": {}}'

6. API Key Security

Risk Level: Medium to High

Firebase API keys are designed to be public—they identify your project but don't grant access alone. However, unrestricted keys can be abused.

The Problem: Unrestricted API Keys

By default, Firebase API keys can be used from any domain, for any API. Attackers can:

  • Exhaust your quotas (denial of service)
  • Use your key for their own projects
  • Access APIs you didn't intend to expose

The Fix: Restrict Your Keys

1. Application Restrictions (GCP Console):

  • Go to GCP Console → APIs & Services → Credentials
  • Click on your API key
  • Under "Application restrictions":
    • Web: Add HTTP referrer restrictions (yourapp.com/*)
    • Mobile: Add package name (Android) or bundle ID (iOS)

2. API Restrictions:

  • Under "API restrictions", select "Restrict key"
  • Only enable the APIs your app actually uses:
    • Firebase Installations API
    • Firebase Cloud Messaging API
    • Cloud Firestore API (if using Firestore)
    • etc.

3. Enable Firebase App Check:

See Section 5: Firebase App Check for complete implementation details. App Check provides an additional layer of protection by ensuring requests come from your legitimate app, not scripts or modified clients.

7. Authentication Configuration

Risk Level: Medium

Firebase Authentication is secure by default, but configuration choices can introduce vulnerabilities.

Check These Settings

1. Anonymous Authentication:

Only enable if you genuinely need it. Anonymous users can:

  • Create unlimited accounts
  • Consume your quotas
  • Make abuse detection harder
// If you must use anonymous auth, link to real accounts
import { linkWithCredential, EmailAuthProvider } from 'firebase/auth';

// When user wants to save their data
const credential = EmailAuthProvider.credential(email, password);
await linkWithCredential(user, credential);

2. Email Enumeration Protection:

Enable in Firebase Console → Authentication → Settings → User Actions:

  • "Email enumeration protection" should be ON

3. Password Requirements:

Firebase allows passwords as short as 6 characters. Add client-side validation:

const validatePassword = (password: string) => {
  const errors = [];
  if (password.length < 12) errors.push('At least 12 characters');
  if (!/[A-Z]/.test(password)) errors.push('At least one uppercase letter');
  if (!/[a-z]/.test(password)) errors.push('At least one lowercase letter');
  if (!/[0-9]/.test(password)) errors.push('At least one number');
  if (!/[^A-Za-z0-9]/.test(password)) errors.push('At least one special character');
  return errors;
};

4. Multi-Factor Authentication:

Enable MFA for sensitive applications:

import { multiFactor, PhoneAuthProvider, PhoneMultiFactorGenerator } from 'firebase/auth';

// Enroll user in MFA
const multiFactorUser = multiFactor(user);
const session = await multiFactorUser.getSession();
const phoneAuthProvider = new PhoneAuthProvider(auth);

const verificationId = await phoneAuthProvider.verifyPhoneNumber(
  { phoneNumber: '+1234567890', session },
  recaptchaVerifier
);

// After user enters verification code
const credential = PhoneAuthProvider.credential(verificationId, verificationCode);
const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(credential);
await multiFactorUser.enroll(multiFactorAssertion, 'Phone Number');

8. Custom Claims and Role-Based Access

Risk Level: Medium

Custom claims are set server-side and embedded in the user's ID token. They're perfect for role-based access control.

Setting Custom Claims

// In a Cloud Function or Admin SDK
await admin.auth().setCustomUserClaims(userId, {
  admin: true,
  role: 'editor',
  organization: 'org-123'
});

Using Claims in Security Rules

// Firestore rules
match /admin/{document=**} {
  allow read, write: if request.auth.token.admin == true;
}

match /organizations/{orgId}/{document=**} {
  allow read, write: if request.auth.token.organization == orgId;
}

Important Considerations

1. Claims are cached in the token:

Users need to refresh their token to see new claims:

// Force token refresh after claims change
await user.getIdToken(true);

2. Keep claims small:

The entire token (including claims) must be under 1000 bytes. Don't store large arrays.

3. Don't trust client-side claims:

Always verify in security rules or server-side:

// WRONG: Trusting client claim
if (user.customClaims?.admin) { /* ... */ }

// RIGHT: Verify server-side
const decoded = await admin.auth().verifyIdToken(token);
if (decoded.admin) { /* ... */ }

9. Firebase Data Connect Security

Risk Level: Medium

Firebase Data Connect is a backend-as-a-service for PostgreSQL databases using GraphQL. It's integrated with Firebase Authentication and requires explicit authorization for all operations.

The @auth Directive

Every query and mutation must have an @auth directive. Without it, operations default to NO_ACCESS—meaning they can only be called from privileged server environments.

# schema.gql

# Access levels from least to most restrictive:
# PUBLIC - Anyone can access (no auth required)
# USER_ANON - Any authenticated user, including anonymous
# USER - Any authenticated user (excludes anonymous)
# USER_EMAIL_VERIFIED - Verified email required
# NO_ACCESS - Server-side only (default)

type Post @table {
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  content: String!
  authorId: String!
}

# Public read access
query GetPost($id: UUID!) @auth(level: PUBLIC) {
  post(id: $id) { id, title, content }
}

# Only authenticated users can create posts
mutation CreatePost($title: String!, $content: String!)
  @auth(level: USER)
{
  post_insert(data: {
    title: $title,
    content: $content,
    authorId_expr: "auth.uid"
  })
}

The @check Directive

For complex authorization logic, use @check with CEL (Common Expression Language) expressions:

# Only the author can update their own post
mutation UpdatePost($id: UUID!, $title: String!)
  @auth(level: USER)
  @check(expr: "this.authorId == auth.uid", message: "Not the author")
{
  post_update(id: $id, data: { title: $title })
}

# Admin-only mutation using custom claims
mutation DeleteAnyPost($id: UUID!)
  @auth(level: USER)
  @check(expr: "auth.token.admin == true", message: "Admin required")
{
  post_delete(id: $id)
}

Data Connect + App Check

Enable App Check attestation for Data Connect:

// Client initialization with App Check
import { initializeApp } from 'firebase/app';
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';
import { getDataConnect, connectDataConnectEmulator } from 'firebase/data-connect';

const app = initializeApp(firebaseConfig);

// Initialize App Check first
initializeAppCheck(app, {
  provider: new ReCaptchaEnterpriseProvider('your-site-key'),
  isTokenAutoRefreshEnabled: true
});

// Data Connect will automatically use App Check tokens
const dataConnect = getDataConnect(app, { connector: 'my-connector' });

10. Firebase Genkit Security

Risk Level: High (for AI-powered applications)

Firebase Genkit is an open-source framework for building AI-powered applications. Security is critical because LLM calls are expensive and can be abused.

The Problem: Unprotected AI Flows

Deployed Genkit flows without authorization policies are publicly accessible:

// DANGEROUS: No auth policy
export const generateContent = ai.defineFlow('generateContent', async (input) => {
  const response = await ai.generate({ prompt: input.prompt });
  return response.text;
});

The Fix: Always Define Authorization Policies

import { onCallGenkit } from 'firebase-functions/https';
import { genkit } from 'genkit';
import { googleAI } from '@genkit-ai/googleai';

const ai = genkit({
  plugins: [googleAI()],
});

// Secure: Requires authenticated user + App Check
export const generateContent = onCallGenkit(
  {
    authPolicy: (auth) => {
      if (!auth) throw new Error('Authentication required');
    },
    consumeAppCheckToken: true,
  },
  ai.defineFlow('generateContent', async (input, { auth }) => {
    // auth.uid is verified
    const response = await ai.generate({
      model: 'googleai/gemini-1.5-flash',
      prompt: input.prompt,
    });
    return response.text;
  })
);

Genkit Security Best Practices

1. Use built-in auth helpers:

import { signedIn, hasClaim } from 'firebase-functions/https';

// Require any authenticated user
export const publicFlow = onCallGenkit(
  { authPolicy: signedIn() },
  myFlow
);

// Require admin claim
export const adminFlow = onCallGenkit(
  { authPolicy: hasClaim('admin') },
  myFlow
);

2. Store API keys in Secret Manager:

// functions/package.json - set secret reference
// functions/.env.local - for local development only

// Access via environment variable
const apiKey = process.env.GOOGLE_AI_API_KEY;

Never hardcode API keys. Use Google Secret Manager for production.

3. Implement context-based tool security:

// Pass user context to tools, don't let AI infer it
ai.defineTool(
  { name: 'getUserData', description: 'Get current user data' },
  async (input, { auth }) => {
    // Use auth.uid from context, not from AI-generated input
    const userData = await db.collection('users').doc(auth.uid).get();
    return userData.data();
  }
);

4. Use lower-privileged SDKs:

When AI tools need to access Firebase services, use user-scoped access rather than Admin SDK when possible to limit blast radius.

11. Testing Your Security Configuration

Before deploying to production, test your security rules systematically.

Using the Firebase Emulator Suite

# Start emulators
firebase emulators:start

# Run security rules tests
firebase emulators:exec "npm test"

Automated Rules Testing

import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment,
} from '@firebase/rules-unit-testing';

let testEnv;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: 'test-project',
    firestore: {
      rules: fs.readFileSync('firestore.rules', 'utf8'),
    },
  });
});

afterAll(async () => {
  await testEnv.cleanup();
});

test('users can only read their own data', async () => {
  const aliceDb = testEnv.authenticatedContext('alice').firestore();
  const bobDb = testEnv.authenticatedContext('bob').firestore();

  // Alice can read her own data
  await assertSucceeds(aliceDb.doc('users/alice').get());

  // Bob cannot read Alice's data
  await assertFails(bobDb.doc('users/alice').get());
});

test('unauthenticated users cannot write', async () => {
  const unauthDb = testEnv.unauthenticatedContext().firestore();
  await assertFails(unauthDb.collection('posts').add({ title: 'Test' }));
});

test('users cannot escalate privileges', async () => {
  const aliceDb = testEnv.authenticatedContext('alice').firestore();

  // User tries to set admin flag
  await assertFails(
    aliceDb.doc('users/alice').update({ isAdmin: true })
  );
});

CI/CD Integration

Add security tests to your pipeline:

# .github/workflows/security-tests.yml
name: Security Tests

on: [push, pull_request]

jobs:
  test-rules:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Install Firebase CLI
        run: npm install -g firebase-tools

      - name: Run security rules tests
        run: firebase emulators:exec "npm test"

Detecting These Issues Automatically

Manually auditing Firebase configurations is time-consuming and error-prone. We built Firebomb, an open source CLI tool specifically for Firebase security testing.

# Install and run
git clone https://github.com/ModernPentest/firebomb.git
cd firebomb

# Discover Firebase config from your app
uv run firebomb discover --url https://your-app.com

# Run comprehensive security tests
uv run firebomb test

# Generate a detailed report
uv run firebomb report --format html --output assessment.html

Firebomb automatically:

  • Extracts Firebase configuration from your frontend
  • Tests Firestore, RTDB, and Storage security rules
  • Checks Cloud Functions for authentication issues
  • Compares anonymous vs. authenticated access
  • Generates professional security reports

Read more in our Introducing Firebomb post.

Security Checklist

Before going to production, verify:

Firestore

  • Test mode rules have been replaced with production rules
  • Every collection has explicit read/write rules
  • Rules use request.auth.uid for user-scoped access
  • Data validation rules prevent privilege escalation
  • No wildcards ({document=**}) with permissive access
  • Sensitive fields are protected from user modification

Realtime Database

  • Root-level .read and .write are false
  • User data is scoped to $uid === auth.uid
  • Data validation rules are in place
  • No unintended cascade from parent rules

Cloud Storage

  • Default rules have been updated
  • File type validation is enforced
  • File size limits are set
  • User uploads are scoped to user paths
  • Executable file types are blocked

Cloud Functions

  • All HTTP functions verify authentication
  • Callable functions check context.auth
  • Input validation is implemented
  • CORS is properly configured
  • Rate limiting is in place
  • App Check enforcement is enabled

App Check

  • App Check is enabled in Firebase Console
  • Attestation providers configured (reCAPTCHA/Play Integrity/App Attest)
  • Enforcement is enabled for Firestore, RTDB, and Storage
  • Cloud Functions use enforceAppCheck: true
  • Debug tokens are only used in development
  • No FIREBASE_APPCHECK_DEBUG_TOKEN in production code

Authentication

  • Anonymous auth is disabled (unless needed)
  • Email enumeration protection is enabled
  • Strong password requirements are enforced
  • MFA is available for sensitive accounts
  • Identity Platform upgrade completed (if using App Check)

API Keys

  • HTTP referrer restrictions are set
  • API restrictions limit to required APIs only
  • Separate keys for development and production

Data Connect (if applicable)

  • All queries and mutations have @auth directive
  • Default access level is NO_ACCESS
  • Complex authorization uses @check directive with CEL
  • App Check is integrated with Data Connect

Genkit (if applicable)

  • All deployed flows have authPolicy defined
  • API keys stored in Secret Manager (not hardcoded)
  • Tools use context-based security (auth.uid)
  • Lower-privileged SDKs used for user operations
  • consumeAppCheckToken: true enabled

Testing

  • Security rules tests exist and run in CI/CD
  • Firebase Emulator Suite is used for local testing
  • Privilege escalation tests are implemented
  • Unauthenticated access tests are implemented

Automate Your Firebase Security

Want continuous protection instead of manual checklists? ModernPentest's Firebase Security Scanning uses AI agents trained specifically on Firebase vulnerabilities to:

  • Scan in under an hour - Full penetration test + SOC 2 report
  • Test all services - Firestore, RTDB, Storage, Functions, and Auth
  • Monitor continuously - Weekly/daily scans catch regressions
  • Generate compliance reports - Auditor-ready documentation for SOC 2, ISO 27001

Our agents use Firebomb under the hood, combined with AI analysis to prioritize findings by actual risk and provide specific remediation guidance.

Start Your Free Security Scan →


This guide is part of our Security Guides series. Follow us for more content on securing modern web applications.

Written by

Victor

Victor

Founder, ModernPentest

ModernPentest

Ready to secure your application?

Get continuous, automated penetration testing for your Supabase, Firebase, or Vercel app. Start your first scan in under 5 minutes.