| name | firebase-security-expert |
| description | Firebase Security Rules auditing, Service Account protection, and App Check integration. Use whenever the user is working with Firebase security — Firestore rules, Realtime Database rules, Storage rules, App Check, service account key management, or Firebase Auth security. Trigger on mentions of Firebase, Firestore, Firebase Security Rules, App Check, service account JSON, Firebase Admin SDK, or when the user asks to review or write Firebase rules. Also trigger for Firebase project security audits.
|
Firebase Security Expert
Comprehensive Firebase security: rules, App Check, service accounts, and auditing.
The #1 Firebase Security Mistake
Exposed service account JSON in client code or public repositories.
firebase-admin-key.json
service-account.json
*-firebase-adminsdk-*.json
echo "*.json" >> .gitignore
echo "service-account*.json" >> .gitignore
echo "*-adminsdk-*.json" >> .gitignore
Firestore Security Rules
Rule Fundamentals
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAuth() {
return request.auth != null;
}
function isOwner(userId) {
return isAuth() && request.auth.uid == userId;
}
function hasRole(role) {
return isAuth() &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == role;
}
function isOrgMember(orgId) {
return isAuth() &&
exists(/databases/$(database)/documents/orgs/$(orgId)/members/$(request.auth.uid));
}
match /users/{userId} {
allow read: if isOwner(userId);
allow create: if isOwner(userId)
&& request.resource.data.keys().hasOnly(['name', 'email', 'createdAt'])
&& request.resource.data.email == request.auth.token.email;
allow update: if isOwner(userId)
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name', 'avatarUrl', 'updatedAt']);
allow delete: if false;
}
match /orgs/{orgId} {
allow read: if isOrgMember(orgId);
allow create: if isAuth();
allow update: if isOrgMember(orgId) && hasRole('admin');
allow delete: if false;
match /projects/{projectId} {
allow read: if isOrgMember(orgId);
allow write: if isOrgMember(orgId)
&& get(/databases/$(database)/documents/orgs/$(orgId)/members/$(request.auth.uid))
.data.role in ['admin', 'member'];
}
}
match /posts/{postId} {
allow read: if resource.data.published == true || isOwner(resource.data.authorId);
allow create: if isAuth()
&& request.resource.data.authorId == request.auth.uid
&& request.resource.data.title is string
&& request.resource.data.title.size() > 0
&& request.resource.data.title.size() <= 200;
allow update: if isOwner(resource.data.authorId)
&& request.resource.data.authorId == resource.data.authorId;
allow delete: if isOwner(resource.data.authorId);
}
match /{document=**} {
allow read, write: if false;
}
}
}
Data Validation in Rules
match /orders/{orderId} {
allow create: if isAuth()
&& validOrder(request.resource.data);
function validOrder(data) {
return data.keys().hasAll(['userId', 'items', 'total', 'status'])
&& data.userId == request.auth.uid
&& data.items is list
&& data.items.size() > 0
&& data.total is number
&& data.total > 0
&& data.status == 'pending';
}
}
Firebase Storage Rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /avatars/{userId}/{allPaths=**} {
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/.*');
}
match /users/{userId}/documents/{docId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
match /orgs/{orgId}/{allPaths=**} {
allow read: if isOrgMember(orgId);
allow write: if isOrgMember(orgId) && hasOrgRole(orgId, 'member');
function isOrgMember(orgId) {
return request.auth != null
&& firestore.exists(/databases/(default)/documents/orgs/$(orgId)/members/$(request.auth.uid));
}
function hasOrgRole(orgId, minRole) {
let member = firestore.get(/databases/(default)/documents/orgs/$(orgId)/members/$(request.auth.uid));
return member.data.role in ['admin', 'owner'];
}
}
match /{allPaths=**} {
allow read, write: if false;
}
}
}
App Check (Anti-Abuse)
App Check ensures only your legitimate apps can access Firebase.
Setup (Next.js + reCAPTCHA v3)
import { initializeApp } from "firebase/app"
import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check"
const app = initializeApp(firebaseConfig)
if (typeof window !== "undefined") {
if (process.env.NODE_ENV === "development") {
(window as any).FIREBASE_APPCHECK_DEBUG_TOKEN = process.env.NEXT_PUBLIC_APPCHECK_DEBUG_TOKEN
}
initializeAppCheck(app, {
provider: new ReCaptchaV3Provider(process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!),
isTokenAutoRefreshEnabled: true,
})
}
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.app.token.valid;
}
}
}
Service Account Best Practices
Use Application Default Credentials (ADC) — Not Key Files
import admin from "firebase-admin"
const serviceAccount = require("./service-account.json")
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })
admin.initializeApp()
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
})
Principle of Least Privilege
roles/firebase.sdkAdminServiceAgent
roles/datastore.user
roles/storage.objectAdmin
Security Audit Checklist
Firestore Rules
Authentication
Service Accounts
App Check
Testing Rules with Firebase Emulator
npm install -g firebase-tools
firebase emulators:start --only firestore,auth
npm install --save-dev @firebase/rules-unit-testing
import { initializeTestEnvironment, assertFails, assertSucceeds } from "@firebase/rules-unit-testing"
const env = await initializeTestEnvironment({
projectId: "test-project",
firestore: { rules: fs.readFileSync("firestore.rules", "utf8") },
})
test("users can only read own profile", async () => {
const alice = env.authenticatedContext("alice")
const bob = env.authenticatedContext("bob")
await assertSucceeds(alice.firestore().doc("users/alice").get())
await assertFails(bob.firestore().doc("users/alice").get())
})
test("unauthenticated users cannot read any data", async () => {
const anon = env.unauthenticatedContext()
await assertFails(anon.firestore().doc("users/alice").get())
})
Key Rules
- NEVER
allow read, write: if true in production — this is the most common Firebase breach
- Service account keys = production passwords — use ADC or secret manager
- Custom claims are server-side only — clients cannot set their own roles
- Validate data in rules — don't rely on client-side validation alone
request.resource.data vs resource.data — incoming vs existing data
- Test with Firebase Emulator before deploying rules
- App Check in enforcement mode — monitoring mode does nothing to block abuse
- Catch-all deny rule at the bottom —
allow read, write: if false
- Audit
get() calls in rules — they count toward your read quota
- Rules Playground in Firebase Console — test rules without deploying