Bola Idor with Bearer Tokens
How BOLA/IdOR Manifests in Bearer Tokens
Bearer tokens are a common authentication mechanism in APIs, but they create a unique vulnerability when combined with BOLA/IdOR (Broken Object Level Authorization / Insecure Direct Object References). The fundamental issue arises from how these tokens are implemented and validated.
When an API receives a Bearer token, it typically extracts user information from the token payload and uses that to authorize access to resources. The vulnerability occurs when the API fails to verify that the authenticated user actually owns or has permission to access the specific resource they're requesting.
Consider this common pattern:
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.sub;
const orderId = req.params.orderId;
// Vulnerable: No check that userId owns orderId
const order = await db.orders.findOne({ id: orderId });
An attacker with a valid Bearer token can exploit this by simply changing the resource identifier in the request. If they're authenticated as user 123 but know (or guess) that user 456 has order 789, they can request /orders/789 and receive the data because the API never validates ownership.
Real-world examples demonstrate this pattern. In 2022, a major e-commerce platform had a vulnerability where authenticated users could access any other user's order history by modifying the order ID in the URL. The API validated the Bearer token but never checked if the requester owned the specific order.
Another common manifestation involves array-based queries:
// Vulnerable: Returns all orders for any ID provided
const orders = await db.orders.find({ userId: decoded.sub, ids: { $in: req.body.orderIds } });
If an attacker sends orderIds: [123, 456, 789] where they only own 123, the API returns all three orders because it trusts the client-provided array without validating each ID.
Batch operations are particularly susceptible. APIs that accept arrays of resource IDs for bulk operations often fail to validate that the authenticated user owns all items in the array:
// Vulnerable batch endpoint
app.post('/users/:userId/documents/batch', async (req, res) => {
const { documentIds } = req.body;
const documents = await Document.find({ _id: { $in: documentIds } });
res.json(documents);
});
An authenticated user can request documents belonging to other users simply by including their document IDs in the array.
Bearer Tokens-Specific Detection
Detecting BOLA/IdOR vulnerabilities in Bearer token implementations requires both manual code review and automated scanning. The key is identifying where resource access checks are missing after token validation.
Manual detection involves searching for patterns where:
- Token validation occurs without subsequent ownership verification
- Database queries use client-provided IDs without checking user ownership
- Batch operations process arrays of IDs without per-item validation
- Resource endpoints accept IDs that could belong to other users
Automated tools like middleBrick can scan Bearer token endpoints for these vulnerabilities. The scanner tests unauthenticated attack surfaces by attempting to access resources across different user contexts, identifying when APIs fail to enforce proper authorization boundaries.
middleBrick's Bearer token-specific detection includes:
| Detection Method | Description | Indicator |
|---|---|---|
| Cross-user resource access | Attempts to access resources using different authenticated contexts | Success when access should be denied |
| ID manipulation testing | Modifies resource identifiers in requests | Unexpected data exposure |
| Batch operation testing | Sends mixed ownership arrays in batch requests | Partial or full data leakage |
| Reference resolution | Analyzes OpenAPI specs for missing authorization checks | Schema-defined but unvalidated endpoints |
During scanning, middleBrick identifies endpoints that accept resource identifiers without proper authorization checks. For example, if an API has an endpoint like /users/{userId}/orders/{orderId}, the scanner tests whether changing userId or orderId allows access to other users' data.
Code analysis can also reveal vulnerabilities. Search for patterns like:
// Red flag: No ownership check
const resource = await Resource.findById(req.params.id);
Or:
// Red flag: Trusting client-provided array
const resources = await Resource.find({ id: { $in: req.body.resourceIds } });
Effective detection requires understanding the business logic and data ownership models specific to your application. What constitutes a valid access pattern versus an attack varies by context.
Bearer Tokens-Specific Remediation
Remediating BOLA/IdOR vulnerabilities in Bearer token implementations requires adding proper ownership validation after token authentication. The core principle is always verify that the authenticated user has rights to the specific resource they're accessing.
Here's a secure pattern for single resource access:
app.get('/orders/:orderId', async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const order = await db.orders.findOne({
_id: req.params.orderId,
userId: decoded.sub // Ensure user owns this order
});
if (!order) {
return res.status(404).json({ error: 'Order not found or unauthorized' });
}
res.json(order);
});
The critical addition is the userId: decoded.sub condition, which ensures the order belongs to the authenticated user.
For batch operations, validate each item individually:
app.post('/batch-orders', async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const { orderIds } = req.body;
// Find orders that belong to this user AND are in the requested list
const orders = await db.orders.find({
_id: { $in: orderIds },
userId: decoded.sub
});
// Verify all requested orders were found
if (orders.length !== orderIds.length) {
return res.status(403).json({ error: 'Unauthorized access to some orders' });
}
res.json(orders);
});
This pattern ensures that only orders belonging to the authenticated user are returned, and if any requested order doesn't belong to them, the entire operation fails.
For more complex authorization scenarios, implement a dedicated authorization service:
class AuthorizationService {
static async canAccessOrder(userId, orderId) {
const order = await db.orders.findOne({ _id: orderId, userId });
return !!order;
}
static async canAccessDocument(userId, documentId, requiredRole = null) {
const document = await db.documents.findOne({ _id: documentId });
if (!document) return false;
// Check ownership
if (document.ownerId !== userId) return false;
// Check role-based access if required
if (requiredRole) {
const user = await db.users.findOne({ _id: userId });
return user.roles.includes(requiredRole);
}
return true;
}
}
Then use it consistently across your API:
app.get('/documents/:documentId', async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const canAccess = await AuthorizationService.canAccessDocument(
decoded.sub,
req.params.documentId
);
if (!canAccess) {
return res.status(403).json({ error: 'Access denied' });
}
const document = await db.documents.findOne({ _id: req.params.documentId });
res.json(document);
});
Middleware can centralize authorization checks:
function requireOwnership(resourceType, resourceIdParam) {
return async (req, res, next) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
let resource;
switch (resourceType) {
case 'order':
resource = await db.orders.findOne({
_id: req.params[resourceIdParam],
userId: decoded.sub
});
break;
case 'document':
resource = await db.documents.findOne({
_id: req.params[resourceIdParam],
ownerId: decoded.sub
});
break;
}
if (!resource) {
return res.status(403).json({ error: 'Access denied' });
}
req.resource = resource;
next();
};
}
// Usage
app.get('/orders/:orderId',
requireOwnership('order', 'orderId'),
(req, res) => {
res.json(req.resource);
}
);
The key remediation principle: never trust client-provided identifiers. Always verify ownership through server-side checks after Bearer token validation.
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 |