Insecure Direct Object Reference in Express with Firestore
Insecure Direct Object Reference in Express with Firestore — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (IDOR) occurs when an API exposes a reference to an object—such as a Firestore document ID—and allows an authenticated user to act on or retrieve another user’s object without proper authorization checks. In an Express application using Firestore, this commonly manifests when route parameters like :documentId are passed directly to Firestore read or write operations without verifying that the authenticated subject has permission for that specific document.
Consider an endpoint designed to fetch a user’s profile document:
app.get('/api/profile/:documentId', async (req, res) => {
const { documentId } = req.params;
const doc = await admin.firestore().doc(`profiles/${documentId}`).get();
if (!doc.exists) return res.status(404).send('Not found');
res.json(doc.data());
});
An authenticated attacker can enumerate predictable IDs (e.g., profile document IDs derived from user UIDs or sequential keys) and access other users’ profiles simply by changing the :documentId in the request. Firestore security rules are not invoked for server-side Admin SDK calls, so the server must enforce ownership checks explicitly. Without these checks, the endpoint is an IDOR vector even if the client-side Firestore rules restrict access.
This risk is compounded when Firestore documents contain sensitive data or when references are exposed in URLs, logs, or client-side JavaScript. Attack techniques include enumeration (trying sequential or known IDs), horizontal privilege escalation (accessing peer resources at the same collection level), and vertical escalation (accessing administrative or shared documents). Because IDOR is about missing authorization logic rather than a flaw in Firestore itself, the fix must be implemented in Express route handlers.
In a middleBrick scan, such endpoints would be flagged under the BOLA/IDOR and Property Authorization checks, with findings mapped to OWASP API Top 10 A1: Broken Object Level Authorization and relevant compliance frameworks. The scanner evaluates runtime behavior and spec definitions, highlighting endpoints where object-level authorization is absent.
Firestore-Specific Remediation in Express — concrete code fixes
To remediate IDOR in Express with Firestore, enforce ownership or role-based checks in your route handlers before performing any Firestore operation. Do not rely on Firestore security rules alone when using the Admin SDK, because those rules apply only to client SDK traffic.
Example 1: Scoped to the authenticated user’s own document
Ensure the document being requested belongs to the authenticated user. If user identity is available from a verified token (e.g., Firebase ID token or an internal session), compare it with the document’s owner field:
app.get('/api/profile/:documentId', async (req, res) => {
const { documentId } = req.params;
const authUserId = req.user.id; // from session or verified token
const docRef = admin.firestore().doc(`profiles/${documentId}`);
const doc = await docRef.get();
if (!doc.exists) return res.status(404).send('Not found');
const data = doc.data();
if (data.userId !== authUserId) {
return res.status(403).send('Forbidden: You can only access your own profile');
}
res.json(data);
});
This pattern confirms that the requesting user matches the owner stored in the document before returning data.
Example 2: Collection group with ownership metadata
For collections where documents may be shared or roles differ, use a Firestore query that scopes to the authenticated user and verifies access at query time:
app.get('/api/notes/:documentId', async (req, res) => {
const { documentId } = req.params;
const authUserId = req.user.id;
const noteRef = admin.firestore().doc(`notes/${documentId}`);
const note = await noteRef.get();
if (!note.exists) return res.status(404).send('Not found');
const noteData = note.data();
// Assume each note has a `userId` field and optionally `sharedWith` array
const isOwner = noteData.userId === authUserId;
const isShared = Array.isArray(noteData.sharedWith) && noteData.sharedWith.includes(authUserId);
if (!isOwner && !isShared) {
return res.status(403).send('Forbidden: You do not have access to this note');
}
res.json(noteData);
});
Example 3: Admin endpoints with role-based access control
For administrative routes, validate a role claim or a dedicated admin document before proceeding:
app.delete('/api/admin/users/:documentId', async (req, res) => {
const { documentId } = req.params;
const authUserId = req.user.id;
const adminRef = admin.firestore().doc(`admins/${authUserId}`);
const adminSnap = await adminRef.get();
if (!adminSnap.exists || !adminSnap.data().isSuper) {
return res.status(403).send('Forbidden: Admin privileges required');
}
await admin.firestore().doc(`users/${documentId}`).delete();
res.status(204).end();
});
These patterns emphasize server-side checks, not only client-side rules. Use consistent identifiers (e.g., userId in documents) and avoid exposing internal keys that can be trivially enumerated.
middleBrick’s BOLA/IDOR and Property Authorization checks highlight endpoints where object-level authorization is missing or inconsistent, helping you prioritize fixes. The CLI can be run as middlebrick scan <url> to detect these issues, and the GitHub Action can enforce a minimum score before merging, while the Web Dashboard tracks improvements over time.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |