The default rules problem
When you create a new Firestore or Realtime Database instance, Firebase gives you two rule options: "locked mode" (deny all) or "test mode" (allow all for 30 days). Most developers choose test mode to move fast, then forget to update it. Even when the 30-day window closes, we've seen teams re-enable open rules because "the app broke."
An open Firestore database means anyone with your project's configuration (which is public — it's in your frontend code) can read, write, and delete any document. No authentication required. For a fintech app, this means user data, transaction records, and financial details are fully exposed.
// DEFAULT TEST MODE — never use in production
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
} Open Firestore rules on a savings platform
During a security assessment of a Nigerian micro-savings app, we accessed the Firestore database directly using the public Firebase config extracted from the APK. All user profiles, savings balances, and withdrawal records were readable and writable without authentication. Over 15,000 user records were exposed.
Writing proper Firestore security rules
Firestore rules should enforce three things: authentication, ownership, and data shape validation. Here's a pattern that covers all three for a typical fintech use case:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only read/write their own profile
match /users/{userId} {
allow read: if request.auth != null && request.auth.uid == userId;
allow write: if request.auth != null
&& request.auth.uid == userId
&& request.resource.data.keys().hasAll(['name', 'email'])
&& request.resource.data.name is string
&& request.resource.data.name.size() <= 100;
}
// Transactions are read-only for the owner, write-only via Cloud Functions
match /transactions/{txId} {
allow read: if request.auth != null
&& resource.data.userId == request.auth.uid;
allow write: if false; // Only Cloud Functions with admin SDK
}
// Deny everything else by default
match /{document=**} {
allow read, write: if false;
}
}
} Key principles: never use wildcard allows, validate data structure in rules where possible, and restrict write access on financial records to server-side Cloud Functions using the Admin SDK (which bypasses rules). This prevents client-side manipulation of balances and transaction records.
Realtime Database rules
If you're using Realtime Database (RTDB) instead of or alongside Firestore, the same principles apply but the syntax differs. RTDB rules use JSON-style validation:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid",
".validate": "newData.hasChildren(['name', 'email'])"
}
},
"transactions": {
"$txId": {
".read": "data.child('userId').val() === auth.uid",
".write": false
}
},
".read": false,
".write": false
}
} RTDB rules cascade — a parent rule that grants access cannot be overridden by a more restrictive child rule. Structure your rules so that the root denies everything, and children grant specific access. Test your rules using the Firebase Emulator Suite before deploying.
Firebase Auth: custom claims for authorization
Firebase Auth handles authentication (who is this user), but authorization (what can this user do) requires custom claims. Don't store roles or permissions in Firestore documents and then check them in rules — that requires a read operation on every request and is easy to get wrong.
Set custom claims via the Admin SDK in a Cloud Function:
// Cloud Function to set admin role
exports.setAdminRole = functions.https.onCall(async (data, context) => {
// Verify the caller is a super-admin
if (!context.auth?.token?.superAdmin) {
throw new functions.https.HttpsError('permission-denied', 'Not authorized');
}
await admin.auth().setCustomUserClaims(data.uid, { admin: true });
return { message: 'Admin role assigned' };
}); Then reference claims directly in security rules: request.auth.token.admin == true. Custom claims are embedded in the ID token and evaluated without additional database reads. Limit custom claims to authorization data — they're capped at 1000 bytes total.
Cloud Functions security
Cloud Functions run with admin privileges by default, bypassing all security rules. This is powerful but dangerous if the functions themselves aren't secured:
- Validate all input — callable functions receive user input. Validate it. Don't trust
datafrom the client any more than you'd trust a raw HTTP request. - Check authentication — verify
context.authexists and check custom claims for authorization. An unauthenticated callable function is an open admin API. - Use least privilege — if a function only reads from Firestore, use a service account scoped to read-only access. Don't give every function full admin credentials.
- Protect HTTP functions — if you expose HTTP-triggered functions (not callable), they're publicly accessible by default. Add authentication middleware or use
functions.https.onCallwhich handles auth automatically.
Using Firebase for your fintech app? We audit Firebase security rules, auth configuration, and Cloud Functions for the exact misconfigurations covered here.
Request a Firebase security auditAPI key restrictions
Firebase API keys are not secrets — they identify your project, not authenticate requests. But unrestricted API keys can be abused for quota exhaustion, unauthorized service access, and billing attacks. Restrict them in the Google Cloud Console:
- Application restrictions — limit by HTTP referrer (web), Android package name, or iOS bundle ID
- API restrictions — limit each key to only the APIs it needs (Firebase Auth, Firestore, Cloud Functions)
- Separate keys per platform — use different API keys for your web app, Android app, and iOS app, each with platform-specific restrictions
Without these restrictions, an attacker who extracts your API key from a decompiled mobile app can use it from any platform for any Firebase service associated with your project.
Firebase App Check
App Check verifies that incoming requests originate from your genuine app, not from scripts, bots, or modified APKs. It uses attestation providers (reCAPTCHA for web, Play Integrity for Android, Device Check for iOS) to validate the request source.
Enable App Check enforcement on Firestore, Realtime Database, Cloud Functions, and Cloud Storage. Without it, anyone with your Firebase config can make API calls directly — even with perfect security rules, automated abuse (scraping, enumeration, quota attacks) remains possible.
App Check is not a replacement for security rules. It's an additional layer that ensures rules are evaluated only for requests from legitimate app instances, not from curl commands or Postman.
Firebase security is your responsibility, not Google's
Firebase's defaults prioritize developer speed. Production security requires explicit rules on every collection, custom claims for authorization, secured Cloud Functions, restricted API keys, and App Check enforcement. If you're building fintech on Firebase, treat these as deployment prerequisites — not post-launch improvements.
Related reading
Blog: Can hackers see your source code? · Fix broken object-level authorization · Security audit before launch
Guides: Mobile app pentesting · Security checklist · OWASP for fintech
Services: Penetration testing · API security · Secure architecture review