Bola Idor in Express with Bearer Tokens
Bola Idor in Express with Bearer Tokens — how this combination creates or exposes the vulnerability
Broken Level of Authorization (BOLA) is a class of API vulnerability where one user can access or modify resources that belong to another user. In Express.js applications that rely on Bearer Tokens for authentication, BOLA often arises when authorization checks are incomplete or inconsistent, even though a valid token proves identity.
Consider a typical setup where an access token identifies a user (e.g., via a subject claim like sub) but the server routes requests such as GET /users/:id without verifying that the :id path parameter matches the subject in the token. An attacker with a stolen or guessed token can simply change the numeric ID in the URL to access another user’s data. This is a BOLA issue: the authentication (token validation) succeeds, but the authorization (resource ownership check) is missing or misapplied.
In Express, this can happen when route handlers directly trust user-supplied identifiers. For example, if your token payload includes sub: "user-123" and you have a route /api/users/:userId, failing to compare req.user.sub with req.params.userId means any authenticated user can iterate over IDs and read others’ profiles, settings, or sensitive records. Common patterns that exacerbate this include:
- Using sequential IDs (1, 2, 3) that are easy to guess.
- Exposing internal database keys in URLs without an access control layer.
- Applying token validation middleware but skipping resource-level ownership checks.
BOLA also intersects with other checks in middleBrick’s 12 parallel scans. For instance, Property Authorization ensures that each field returned from the server is appropriate for the token’s scope, while Input Validation ensures that IDs and parameters conform to expected formats before being used in database queries. Together, these highlight that BOLA in Express with Bearer Tokens is not just about verifying the token, but about consistently enforcing that the authenticated subject is allowed to operate on the requested resource.
Real-world analogies include scenarios where an API endpoint like GET /api/invoices/:invoiceId does not check whether invoiceId belongs to the authenticated tenant or user. Even with a valid Bearer Token, missing tenant or user context checks enable horizontal privilege escalation across accounts.
Bearer Tokens-Specific Remediation in Express — concrete code fixes
To fix BOLA in Express when using Bearer Tokens, enforce that every resource request validates the relationship between the token’s subject and the requested resource identifier. Below are concrete, secure patterns and code examples.
1. Validate ownership in route handlers
Always compare the authenticated subject from the token with the resource identifier before proceeding. For example:
// middleware/authenticate.js (pseudo, e.g., using express-jwt or a custom verify)
function authenticate(req, res, next) {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
const token = auth.slice(7);
try {
const payload = verifyToken(token); // your JWT verify function
req.user = payload; // { sub: 'user-123', scope: 'read:users' }
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// routes/users.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('./middleware/authenticate');
router.get('/users/:userId', authenticate, (req, res) => {
const requestingUserId = req.user.sub; // from Bearer Token payload
const targetUserId = req.params.userId;
if (requestingUserId !== targetUserId) {
return res.status(403).json({ error: 'Forbidden: cannot access other user resources' });
}
// proceed to fetch and return user data
res.json({ id: targetUserId, name: 'Alice' });
});
module.exports = router;
2. Use centralized authorization helpers
Encapsulate the check to avoid repeating logic and reduce mistakes:
// middleware/ensureOwnership.js
function ensureOwnership(req, res, next) {
const subject = req.user?.sub;
const id = req.params.id || req.params.userId;
if (!subject || subject !== id) {
return res.status(403).json({ error: 'Forbidden: insufficient permissions' });
}
next();
}
// routes/profile.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('./middleware/authenticate');
const ensureOwnership = require('./middleware/ensureOwnership');
router.get('/profile/:userId', authenticate, ensureOwnership, (req, res) => {
// safe to proceed: subject matches userId
res.json({ email: '[email protected]' });
});
module.exports = router;
3. Apply scope-based checks for different resources
For endpoints that involve collections (e.g., invoices), verify tenant or user context explicitly:
// routes/invoices.js
router.get('/tenants/:tenantId/invoices/:invoiceId', authenticate, (req, res) => {
const tenantId = req.params.tenantId;
const userId = req.user.sub; // assume token includes tenant association
// Ensure user belongs to tenant before accessing invoice
if (!userBelongsToTenant(userId, tenantId)) {
return res.status(403).json({ error: 'Forbidden: user not in tenant' });
}
// fetch invoice for tenant
res.json({ invoiceId: req.params.invoiceId, amount: 100 });
});
function userBelongsToTenant(userId, tenantId) {
// check database or directory
return true; // simplified
}
4. Enforce strict ID formats and canonical IDs
Use UUIDs or opaque identifiers instead of sequential integers to reduce guessability, and always normalize IDs before comparison:
const { v4: uuidv4 } = require('uuid');
app.post('/users', (req, res) => {
const id = uuidv4(); // e.g., 'a1b2c3d4-...'
// store user with this id
res.status(201).json({ id });
});
// Then in route:
router.get('/users/:userId', authenticate, (req, res) => {
const normalizedId = normalizeId(req.params.userId);
if (req.user.sub !== normalizedId) {
return res.status(403).json({ error: 'Forbidden' });
}
// safe
});
5. Complement with middleware for input validation and property-level checks
Use express-validator or similar to ensure IDs are well-formed, and apply property authorization to limit returned fields based on scope:
const { body, param, validationResult } = require('express-validator');
router.get(
'/users/:userId',
authenticate,
param('userId').isUUID(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
if (req.user.sub !== req.params.userId) {
return res.status(403).json({ error: 'Forbidden' });
}
// proceed safely
res.json({ id: req.params.userId, name: 'Bob' });
}
);
These patterns ensure that Bearer Token authentication is paired with strict ownership checks, reducing the risk of BOLA in Express APIs.
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 |