Broken Access Control in Express with Mongodb
Broken Access Control in Express with Mongodb — how this specific combination creates or exposes the vulnerability
Broken Access Control in an Express + Mongodb stack typically occurs when authorization checks are missing or inconsistent at the route level and are not enforced by the database layer. Because Mongodb stores documents as rich structures, developers sometimes rely on application-side filters alone (e.g., matching req.user.id to a resource owner ID) without constraining queries with proper ownership or role predicates. This creates BOLA/IDOR conditions where an authenticated user can modify or retrieve another user’s data by changing an identifier in the request (e.g., /users/:id/profile).
Express itself does not enforce data permissions; it provides routing and middleware. If route handlers directly use user-supplied parameters (like req.params.id or query filters) to build Mongodb queries without validating that the requesting user has the right to access that document, the API surface is vulnerable. For example, a GET /api/users/:id handler that does db.collection('users').findOne({ _id: new ObjectId(req.params.id) }) without confirming the requester owns that _id exposes a BOLA flaw. Compounding this, permissive CORS or missing scoped middleware can allow broader unintended access across origins or services.
Additionally, if queries rely on client-supplied filters (e.g., { role: req.query.role }) without server-side role validation, privilege escalation becomes feasible. A regular user could inject isAdmin=true if the application does not sanitize and enforce role checks against the authenticated subject. Because Mongodb can return many fields by default, sensitive properties may be unintentionally exposed if field-level projection is not applied. These patterns align with the OWASP API Top 10 category for Broken Object Level Authorization and commonly appear in implementations that skip centralized authorization logic and strict query scoping.
middleBrick scans such endpoints using its BOLA/IDOR checks combined with Property Authorization and Input Validation tests, validating that object-level permissions are enforced and that untrusted input cannot alter query semantics. Findings include insecure direct object references, missing ownership predicates, and over-permissive query filters, each mapped to remediation guidance and compliance mappings like OWASP API Top 10 and SOC2 controls.
Mongodb-Specific Remediation in Express — concrete code fixes
To secure Express routes that use Mongodb, always enforce ownership and role checks in the query itself and avoid trusting client-supplied identifiers for access decisions. Use middleware to resolve the authenticated subject (e.g., from a session or JWT) and ensure every database operation includes that subject as a required filter component.
Example: Safe user profile endpoint with ownership check
const { MongoClient, ObjectId } = require('mongodb');
const express = require('express');
const app = express();
const client = new MongoClient('mongodb://localhost:27017');
async function getUserProfile(req, res) {
const userId = req.user.sub; // authenticated subject from middleware, e.g., JWT payload
const targetId = req.params.id;
if (!ObjectId.isValid(targetId)) {
return res.status(400).json({ error: 'Invalid user id' });
}
const database = client.db('appdb');
const user = await database.collection('users').findOne({
_id: new ObjectId(targetId),
// Ownership enforced: only return docs where the document owner matches the authenticated user
ownerId: userId
}, {
// Explicitly limit returned fields to avoid sensitive data exposure
projection: { email: 1, profile: 1, _id: 1 }
});
if (!user) {
return res.status(404).json({ error: 'Not found' });
}
res.json(user);
}
app.get('/api/users/:id', (req, res) => {
getUserProfile(req, res).catch(() => res.status(500).json({ error: 'Internal error' }));
});
Example: Role-based access with server-side enforcement
async function updateSettings(req, res) {
const userId = req.user.sub;
const targetId = req.params.id;
const { preferenceKey, preferenceValue } = req.body;
if (!ObjectId.isValid(targetId)) {
return res.status(400).json({ error: 'Invalid id' });
}
const database = client.db('appdb');
// Ensure the requester is an admin or is updating their own settings
const user = await database.collection('users').findOne({
_id: new ObjectId(targetId),
$or: [
{ role: 'admin' },
{ _id: new ObjectId(userId) }
]
});
if (!user) {
return res.status(403).json({ error: 'Forbidden' });
}
const result = await database.collection('settings').updateOne(
{ userId: userId, key: preferenceKey },
{ $set: { value: preferenceValue, updatedAt: new Date() } },
{ upsert: true }
);
res.json({ modifiedCount: result.modifiedCount, upsertedId: result.upsertedId });
}
General practices
- Always resolve the authenticated identity server-side (e.g., from a verified JWT or session) and include it in every query predicate.
- Use ObjectId.isValid on user-supplied IDs before using them in queries to avoid injection or malformed document lookups.
- Apply field projection to limit returned data and reduce exposure of sensitive attributes.
- Do not pass raw user input directly as query keys or values; validate and transform input before using it in database operations.
- Prefer parameterized updates and upserts with explicit ownership filters rather than client-supplied filter objects that can be manipulated.
- Consider a centralized authorization layer or policy library to keep checks consistent across routes.
middleBrick’s CLI can be used to verify these fixes by scanning endpoints after changes: middlebrick scan https://api.example.com. The GitHub Action can enforce a minimum score in CI, and the MCP Server allows scanning from within AI coding assistants to catch regressions early.