Bola Idor in Express
How Bola Idor Manifests in Express
BOLA/IdOR (Broken Object Level Authorization) in Express applications often stems from how routes handle dynamic parameters and user context. Express's middleware architecture and parameter handling create specific patterns where this vulnerability frequently appears.
The most common Express-specific manifestation occurs in route handlers that use req.params or req.query without validating whether the authenticated user owns the resource being accessed. Consider a typical Express route:
app.get('/api/users/:userId', (req, res) => {
const userId = req.params.userId;
db.query('SELECT * FROM users WHERE id = ?', [userId])
.then(user => res.json(user))
.catch(err => res.status(500).json({ error: 'Database error' }));
});This pattern is dangerous because Express automatically parses URL parameters and makes them available on req.params. The route trusts that userId from the URL belongs to the authenticated user, but there's no verification. An attacker can simply change the :userId parameter to access any user's data.
Express middleware also creates specific BOLA/IdOR scenarios. Authentication middleware often attaches user information to req.user, but subsequent route handlers may ignore this context:
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (!err) req.user = user;
next();
});
} else {
next();
}
}
app.get('/api/orders/:orderId', authMiddleware, (req, res) => {
const orderId = req.params.orderId;
db.query('SELECT * FROM orders WHERE id = ?', [orderId])
.then(order => res.json(order))
.catch(err => res.status(500).json({ error: 'Database error' }));
});Even with authentication middleware, the route handler doesn't check if req.user.id matches the order's owner. Express's permissive routing allows attackers to enumerate resources by simply changing URL parameters.
Another Express-specific pattern involves population in Mongoose queries, common in Express + MongoDB applications:
app.get('/api/users/:userId/posts', authMiddleware, (req, res) => {
Post.find({ userId: req.params.userId })
.populate('comments.author', 'name email')
.exec((err, posts) => {
if (err) return res.status(500).json({ error: 'Error fetching posts' });
res.json(posts);
});
});Here, an attacker can access any user's posts by changing :userId, and the populate operation may also expose comment authors' PII without proper authorization checks.
Express's flexible routing can also lead to BOLA/IdOR through route overlap. Consider:
app.get('/api/users/:id', getUser);
app.get('/api/users/profile', getProfile);
function getUser(req, res) {
const id = req.params.id;
User.findById(id, (err, user) => {
if (err || !user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
}
function getProfile(req, res) {
const userId = req.user.id;
User.findById(userId, (err, user) => {
if (err || !user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
}An attacker can access /api/users/:id with any user ID, bypassing the intended profile-only access pattern.
Express-Specific Detection
Detecting BOLA/IdOR in Express applications requires understanding Express's routing patterns and middleware flow. middleBrick's Express-specific scanning looks for several key indicators.
The scanner analyzes route definitions to identify dynamic parameters that could lead to authorization bypasses. For Express applications, it specifically examines:
- Route patterns with
:paramsyntax that reference user-owned resources - Middleware chains that may bypass authentication for certain paths
- Database queries that use URL parameters directly without ownership validation
- Population operations that could expose related user data
When scanning an Express API, middleBrick tests for BOLA/IdOR by manipulating URL parameters and observing responses. For example, given a route like:
app.get('/api/users/:userId/orders', (req, res) => {
const userId = req.params.userId;
Order.find({ userId })
.then(orders => res.json(orders))
.catch(err => res.status(500).json({ error: 'Error fetching orders' }));
});The scanner will:
- Authenticate as one user and capture their
userId - Request
/api/users/:userId/orderswith the authenticated user's ID (baseline) - Request the same endpoint with different user IDs
- Compare response patterns to detect if data from other users is accessible
middleBrick's Express detection also examines error handling patterns. Express applications often return different error messages based on resource existence, which can aid enumeration:
app.get('/api/users/:userId', (req, res) => {
User.findById(req.params.userId, (err, user) => {
if (err || !user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
});The scanner tests how the API responds to non-existent vs. inaccessible resources, looking for timing differences or error message variations that could indicate BOLA/IdOR vulnerabilities.
For Express applications using Mongoose, middleBrick analyzes population patterns:
app.get('/api/posts/:postId', (req, res) => {
Post.findById(req.params.postId)
.populate('author', 'name email bio')
.exec((err, post) => {
if (err || !post) return res.status(404).json({ error: 'Post not found' });
res.json(post);
});
});The scanner checks if the authenticated user can access posts they don't own and whether population exposes author information without proper authorization.
middleBrick's CLI tool makes Express BOLA/IdOR scanning straightforward:
npx middlebrick scan https://api.example.com --output json --tests bola
# Or scan with specific Express patterns
npx middlebrick scan https://api.example.com --output json --tests bola,authentication
The GitHub Action integration allows continuous monitoring of Express APIs in your CI/CD pipeline:
name: API Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run middleBrick Scan
run: |
npx middlebrick scan https://staging.example.com/api --fail-on-score-below CExpress-Specific Remediation
Remediating BOLA/IdOR in Express requires implementing proper authorization checks at the route handler level. The key principle: never trust URL parameters or query strings for resource ownership.
The most effective Express-specific pattern uses middleware to enforce resource ownership:
function authorizeResourceOwner(resourceField = 'userId') {
return (req, res, next) => {
const resourceId = req.params[resourceField];
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// For numeric IDs, compare directly
if (req.user.id === resourceId) {
return next();
}
// For database lookups, verify ownership
const model = getModelFromField(resourceField);
model.findById(resourceId, (err, resource) => {
if (err || !resource) {
return res.status(404).json({ error: 'Resource not found' });
}
if (resource.ownerId.toString() !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
next();
});
};
}
// Usage in routes
app.get('/api/users/:userId', authMiddleware, authorizeResourceOwner('userId'), (req, res) => {
User.findById(req.params.userId, (err, user) => {
if (err || !user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
});
app.get('/api/users/:userId/orders', authMiddleware, authorizeResourceOwner('userId'), (req, res) => {
Order.find({ userId: req.params.userId }, (err, orders) => {
if (err) return res.status(500).json({ error: 'Error fetching orders' });
res.json(orders);
});
});This pattern ensures that the authenticated user's ID matches the resource owner ID before allowing access to the main route handler.
For Express applications using Mongoose, leverage pre-hooks for authorization:
const orderSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, required: true },
amount: { type: Number, required: true },
items: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Item' }]
});
orderSchema.pre('find', function(next) {
if (this.op === 'find' && this.getFilter().userId !== req.user.id) {
this.getFilter().userId = req.user.id;
}
next();
});
orderSchema.pre('save', function(next) {
if (!this.isNew) {
return next(new Error('Modifying existing orders is not permitted'));
}
this.userId = req.user.id;
next();
});This ensures that all queries automatically filter by the authenticated user's ID, preventing BOLA/IdOR at the database level.
Express's error handling can be standardized to prevent information leakage:
app.use((err, req, res, next) => {
if (err.name === 'CastError' || err.name === 'ValidationError') {
return res.status(400).json({ error: 'Invalid request' });
}
if (err.message.includes('not found')) {
return res.status(404).json({ error: 'Resource not found' });
}
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});This prevents attackers from distinguishing between non-existent resources and authorization failures.
For population operations, implement authorization-aware population:
function populateWithAuth(path, select) {
return function(next) {
this.populate({
path,
select,
match: { ownerId: req.user.id }
});
next();
};
}
app.get('/api/posts/:postId', authMiddleware, (req, res) => {
Post.findById(req.params.postId)
.populate('author comments.author', 'name email')
.exec((err, post) => {
if (err || !post || post.author.id !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
res.json(post);
});
});The middleBrick dashboard helps track remediation progress by showing security score improvements over time as you fix BOLA/IdOR issues across your Express API endpoints.
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 |