Insecure Direct Object Reference in Express with Mongodb
Insecure Direct Object Reference in Express with Mongodb — how this specific combination creates or exposes the vulnerability
Insecure Direct Object Reference (BOLA/IDOR) occurs when an API exposes internal object references—such as database IDs—and allows an attacker to manipulate them to access or modify data without authorization. In an Express application using MongoDB, this commonly surfaces through route parameters like /users/:userId or /records/:recordId where the controller directly uses req.params.userId to query the database.
Consider an endpoint that retrieves a user profile:
app.get('/users/:userId', async (req, res) => {
const user = await db.collection('users').findOne({ _id: req.params.userId });
res.json(user);
});
If the API does not verify that the authenticated user is allowed to view the requested userId, an attacker can change the ID and enumerate other users’ records. Because MongoDB ObjectIds are predictable and often exposed directly in URLs and API responses, this combination makes IDOR straightforward to exploit. Attackers may use tools to iterate through valid ObjectIds and harvest data belonging to other users.
Another common pattern is using a non-sequential identifier (e.g., a UUID or a custom string) that the developer assumes is unguessable. Even when using such identifiers, without proper ownership checks, the endpoint remains vulnerable. The risk is compounded when the MongoDB query does not include tenant or ownership filters, effectively exposing a horizontal IDOR issue across all users sharing the same database collection.
In more complex scenarios, related data may be referenced by different keys. For example, an endpoint might accept a postId and fetch a post without confirming the poster’s identity:
app.get('/posts/:postId', async (req, res) => {
const post = await db.collection('posts').findOne({ _id: req.params.postId });
res.json(post);
});
Because the query lacks an authorization check that ties the post to the requesting user’s permissions, this is a classic BOLA/IDOR vulnerability. middleBrick detects such patterns during unauthenticated scanning by correlating OpenAPI paths with runtime behavior and flagging endpoints that expose direct object references without corresponding access controls.
Additional risk arises when the API exposes secondary references, such as a user’s role or scope, that should be enforced server-side. An attacker who can supply arbitrary IDs for related resources—such as changing a groupId in a request—might gain elevated access to data or operations. This is particularly relevant in MongoDB when references are stored as strings or ObjectIds and not validated against the authenticated subject’s permissions.
Mongodb-Specific Remediation in Express — concrete code fixes
To mitigate IDOR in Express with MongoDB, always enforce authorization checks on the server side using the authenticated subject’s identity. Instead of trusting client-supplied identifiers, derive the accessible data scope from the authenticated context. Below are concrete, safe patterns.
1. Include ownership or tenant filtering in every query. For example, if each document contains a userId field that indicates ownership, include it in the query:
app.get('/users/:userId', async (req, res) => {
const authenticatedUserId = req.user.id; // from session or JWT
const requestedUserId = req.params.userId;
if (authenticatedUserId !== requestedUserId) {
return res.status(403).json({ error: 'Forbidden' });
}
const user = await db.collection('users').findOne({
_id: requestedUserId,
userId: authenticatedUserId
});
res.json(user);
});
This ensures that even if an attacker changes :userId, the query will not return another user’s document because the filter includes the authenticated user’s ID.
2. Use a mapping between public identifiers and internal IDs. Instead of exposing MongoDB ObjectIds directly, maintain a mapping table (or field) that links public identifiers to internal records, and scope queries by the subject’s permissions:
app.get('/records/:publicId', async (req, res) => {
const subjectId = req.user.id;
const publicId = req.params.publicId;
const record = await db.collection('records').findOne({
publicId: publicId,
allowedSubjects: subjectId
});
if (!record) {
return res.status(404).json({ error: 'Not found' });
}
res.json(record);
});
Here, the allowedSubjects array stores identifiers of users or groups that can access the record. This pattern works well for many-to-many relationships and prevents horizontal IDOR.
3. Enforce authorization after data retrieval when necessary. If a direct ownership check in the query is not feasible, retrieve the document and validate permissions in code before returning it:
app.get('/posts/:postId', async (req, res) => {
const post = await db.collection('posts').findOne({ _id: req.params.postId });
if (!post) {
return res.status(404).json({ error: 'Not found' });
}
const subjectId = req.user.id;
const isAuthor = post.authorId === subjectId;
const isAdmin = req.user.roles?.includes('admin');
if (!isAuthor && !isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(post);
});
While this approach works, prefer filtering at the database layer to reduce data exposure and improve performance. middleBrick’s checks include verifying that queries incorporate authorization context and that endpoints with direct object references include either server-side filters or explicit access validation.
Finally, review your OpenAPI spec to ensure that paths using IDs include security schemes and that responses do not inadvertently leak references that should be protected. In CI/CD, the Pro plan’s GitHub Action can flag endpoints that lack ownership checks before deployment.
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 |