firebase-security-expert — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited firebase-security-expert (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Comprehensive Firebase security: rules, App Check, service accounts, and auditing.
Exposed service account JSON in client code or public repositories.
# ❌ DO NOT commit service account keys
firebase-admin-key.json
service-account.json
*-firebase-adminsdk-*.json
# Add to .gitignore immediately
echo "*.json" >> .gitignore # Too broad — use specific names
echo "service-account*.json" >> .gitignore
echo "*-adminsdk-*.json" >> .gitignore
# If already committed: rotate the key immediately in Firebase Console
# Google Cloud → IAM → Service Accounts → Your SA → Keys → Delete old, Add newrules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
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));
}
// Users can only read/write their own profile
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']); // can't change email/role via client
allow delete: if false; // no self-deletion — use Cloud Function
}
// Organization-scoped documents
match /orgs/{orgId} {
allow read: if isOrgMember(orgId);
allow create: if isAuth();
allow update: if isOrgMember(orgId) && hasRole('admin');
allow delete: if false;
// Sub-collection inherits org membership requirement
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'];
}
}
// Public read, authenticated write
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; // can't change author
allow delete: if isOwner(resource.data.authorId);
}
// Deny everything else
match /{document=**} {
allow read, write: if false;
}
}
}// Validate field types and values 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'; // clients can only create pending orders
}
}rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// User avatars — users can only write their own
match /avatars/{userId}/{allPaths=**} {
allow read: if true; // public avatars OK
allow write: if request.auth != null
&& request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024 // 5MB max
&& request.resource.contentType.matches('image/.*');
}
// Private user documents
match /users/{userId}/documents/{docId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Organization files
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']; // simplified
}
}
// Deny everything else
match /{allPaths=**} {
allow read, write: if false;
}
}
}App Check ensures only your legitimate apps can access Firebase.
// lib/firebase/app-check.ts
import { initializeApp } from "firebase/app"
import { initializeAppCheck, ReCaptchaV3Provider } from "firebase/app-check"
const app = initializeApp(firebaseConfig)
// Client-side only
if (typeof window !== "undefined") {
if (process.env.NODE_ENV === "development") {
// Use debug token in dev
(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,
})
}// Firestore rules: enforce App Check
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
// Require valid App Check token
allow read, write: if request.app.token.valid;
}
}
}// ❌ Don't do this in production
import admin from "firebase-admin"
const serviceAccount = require("./service-account.json") // KEY FILE
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })
// ✅ Use ADC (works on Cloud Run, GCE, Cloud Functions automatically)
admin.initializeApp()
// Or via environment variable:
// GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json (for local dev only)
// ✅ Or use individual env vars (safest for most Node.js hosts)
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'),
}),
})# In Google Cloud Console → IAM:
# Don't use "Owner" or "Editor" for service accounts
# Use specific roles:
roles/firebase.sdkAdminServiceAgent # Firebase Admin SDK
roles/datastore.user # Firestore read/write
roles/storage.objectAdmin # Storage admin
# NOT: roles/owner or roles/editorallow read, write: if true)/{document=**} catch-all DENIES by defaultuserId fieldsgit log --all -- "*.json")# Install and run emulator
npm install -g firebase-tools
firebase emulators:start --only firestore,auth
# Run rules tests
npm install --save-dev @firebase/rules-unit-testing// firestore.rules.test.ts
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())
})allow read, write: if false~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.