Identification Failures in Express with Firestore
Identification Failures in Express with Firestore — how this specific combination creates or exposes the vulnerability
Identification failures occur when an API cannot reliably distinguish one user from another, enabling unauthorized access to or manipulation of other users' resources. In Express applications that use Cloud Firestore as a backend, this typically maps to the BOLA (Broken Level Authorization) and IDOR categories in the scan. Firestore security rules and data modeling choices interact directly with how Express routes validate resource ownership, and weak patterns here create conditions where one user can reference or modify another user’s document by changing an identifier.
Consider an Express route like /users/:userId/profile. If the route retrieves the Firestore document using req.params.userId without verifying that the authenticated request’s subject matches that ID, the endpoint is vulnerable. Firestore does not inherently enforce ownership; it enforces rules at the document and collection level. If rules are written to allow broad read or write access based on path patterns (e.g., allowing a user to write to users/{userId}), but the Express layer does not confirm the caller’s identity aligns with userId, an attacker can substitute any userId and potentially access or update other profiles. This is an identification failure because the application fails to bind the authenticated identity to the resource identifier.
Another common pattern exacerbates this when Firestore document IDs are not opaque tokens but predictable values (e.g., email, UID, or sequential IDs). Predictable IDs make it trivial to enumerate or iterate over resources. Even when Firestore rules restrict reads to documents where a field equals the authenticated UID, an attacker who can guess or iterate IDs may still trigger successful reads if the Express route does not enforce an additional authorization check before returning data. The scan’s BOLA/IDOR check will flag this because the runtime behavior shows accessible endpoints where resource identification is not tied to the requester’s verified identity.
Data modeling choices in Firestore can also contribute. For example, storing user-specific data in a top-level collection with permissive rules (e.g., allowing read if request.auth != null) without scoping to a user-specific field invites identification failures. If an endpoint lists documents in users and rules permit listing, an attacker may enumerate records. Proper modeling would scope data access to a user’s subcollection or partition data so that listing is not broadly allowed and each read is validated against the authenticated UID in both rules and Express logic.
In summary, identification failures in Express with Firestore arise when route parameters or identifiers are used directly to fetch or modify Firestore documents without ensuring the authenticated principal matches the intended resource owner. Firestore rules can provide a safety net, but they must be aligned with application-level checks. The scanner’s BOLA/IDOR tests simulate unauthenticated and authenticated context switching to detect whether changing identifiers leads to unauthorized data access, highlighting routes where identification is insufficiently enforced.
Firestore-Specific Remediation in Express — concrete code fixes
Remediation focuses on binding the authenticated identity to the Firestore document reference in Express and tightening Firestore rules to enforce ownership at the database level. Below are concrete, realistic code examples.
Express route with proper ownership check
Use the authenticated UID from the request (e.g., via session or JWT) and ensure it matches the document path. Avoid relying solely on req.params.userId for access decisions.
const express = require('express');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
initializeApp();
const db = getFirestore();
const router = express.Router();
// GET /profile — safely retrieve the authenticated user's own profile
router.get('/profile', async (req, res) => {
// Assume req.user contains verified authentication data with UID
const { uid } = req.user;
if (!uid) {
return res.status(401).json({ error: 'Unauthorized' });
}
const docRef = db.collection('users').doc(uid);
const doc = await docRef.get();
if (!doc.exists) {
return res.status(404).json({ error: 'Not found' });
}
res.json({ id: doc.id, ...doc.data() });
});
// PUT /profile — safely update the authenticated user's own profile
router.put('/profile', async (req, res) => {
const { uid } = req.user;
if (!uid) {
return res.status(401).json({ error: 'Unauthorized' });
}
const docRef = db.collection('users').doc(uid);
await docRef.update(req.body);
res.json({ message: 'Profile updated' });
});
module.exports = router;
Firestore security rules aligned with Express checks
Rules should scope writes and reads to documents where the document ID matches the authenticated UID. This provides a defense-in-depth layer alongside Express checks.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
Avoiding predictable IDs and over-permissive rules
Do not allow broad listing or wildcard matching that bypasses ownership checks. For example, avoid rules like allow read: if request.auth != null; on the users collection without further scoping. Instead, prefer subcollections for user-owned data and ensure listing endpoints are not exposed or are strictly limited.
// Prefer scoping reads to owned documents only
match /users/{userId} {
allow read: if request.auth != null && request.auth.uid == userId;
// Do not allow list-all on this collection via rules
}
Parameterized queries in Express
When querying collections, always filter by the authenticated UID rather than retrieving and filtering client-side. This reduces data exposure and ensures Firestore does not return unintended documents.
router.get('/messages', async (req, res) => {
const { uid } = req.user;
const snapshot = await db.collection('messages')
.where('userId', '==', uid)
.get();
const messages = snapshot.docs.map(d => ({ id: d.id, ...d.data() }));
res.json(messages);
});
By combining Express-level identity validation with tightly scoped Firestore rules and queries, you mitigate identification failures and ensure that users can only access resources they are explicitly authorized to use.