Broken Access Control with Bearer Tokens
How Broken Access Control Manifests in Bearer Tokens
Broken Access Control in Bearer Tokens occurs when attackers exploit token-based authentication to access resources they shouldn't have permission to view or modify. Unlike session-based authentication, Bearer Tokens are self-contained and stateless, making them particularly vulnerable to certain attack patterns.
The most common manifestation is IDOR (Insecure Direct Object Reference) attacks. When an API endpoint uses user-supplied IDs without proper authorization checks, attackers can simply modify token claims or request parameters to access other users' data. For example, an endpoint like /api/users/{id}/profile might return any user's profile if the Bearer Token isn't properly validated against the requested resource.
Privilege escalation is another critical vector. If your Bearer Token contains role-based claims (like 'role: user' vs 'role: admin'), but your API endpoints don't validate these claims before performing sensitive operations, attackers can modify their token to elevate privileges. A common mistake is checking authentication status but not authorization level before allowing administrative actions.
Token scope abuse represents a more subtle attack. Many APIs use OAuth2 scopes to limit what actions a token can perform. However, if the API doesn't validate that the token's scope includes the requested operation, an attacker with a read-only token might modify their token to include 'write' or 'admin' scopes and execute unauthorized operations.
Here's a vulnerable pattern in Node.js/Express:
app.get('/api/users/:userId', (req, res) => {
const userId = req.params.userId;
// BUG: No check that token belongs to this user
User.findById(userId).then(user => res.json(user));
});
The fix requires validating that the authenticated user ID matches the requested resource:
app.get('/api/users/:userId', authenticateToken, (req, res) => {
const requestedUserId = req.params.userId;
const authenticatedUserId = req.user.id; // from JWT payload
if (requestedUserId !== authenticatedUserId) {
return res.status(403).json({ error: 'Access denied' });
}
User.findById(requestedUserId).then(user => res.json(user));
});
Token theft and replay attacks are particularly dangerous with Bearer Tokens since they're designed to be portable. If an attacker intercepts a token (via XSS, MITM, or database breach), they can use it immediately without additional credentials. Unlike session cookies with HTTP-only flags, Bearer Tokens often travel in Authorization headers that are more exposed to client-side attacks.
Another manifestation is improper token revocation. Since Bearer Tokens are stateless, there's no built-in mechanism to invalidate them before expiration. If an attacker obtains a token, they can use it until it expires, regardless of whether the legitimate user changes their password or logs out.
Bearer Tokens-Specific Detection
Detecting Broken Access Control in Bearer Token implementations requires both static code analysis and dynamic testing. The most effective approach combines automated scanning with manual penetration testing.
Automated detection focuses on identifying vulnerable patterns in your codebase. Tools like middleBrick scan your API endpoints without requiring credentials, testing the unauthenticated attack surface. For Bearer Token-specific vulnerabilities, middleBrick's BOLA (Broken Object Level Authorization) module attempts to access resources across different user contexts by manipulating token claims and request parameters.
Here's what effective Bearer Token detection looks like:
middlebrick scan https://api.example.com --category=bola
This command tests for IDOR vulnerabilities by attempting to access resources with modified user IDs in the token payload and request parameters. The scanner maintains a database of user IDs from previous successful authentications and systematically tests cross-user access patterns.
Manual testing techniques include:
- Modifying the 'sub' (subject) claim in JWT tokens to impersonate other users
- Changing role/scope claims to escalate privileges
- Testing endpoints with expired tokens to check for proper rejection
- Verifying that token revocation actually prevents access
Code-level detection requires reviewing authentication middleware. Look for patterns where:
// VULNERABLE: Only checks if token exists, not if user can access resource
app.use('/admin', authenticateToken, adminRoutes);
// SECURE: Validates both authentication and specific permissions
app.use('/admin', authenticateToken, checkAdminPermission, adminRoutes);
Static analysis tools can flag these patterns automatically. For Bearer Tokens specifically, watch for:
- Missing authorization checks after successful authentication
- Direct database queries using client-supplied IDs without ownership verification
- Insufficient scope validation for OAuth2-protected endpoints
Runtime detection involves monitoring for anomalous access patterns. If a user suddenly accesses resources from multiple geographic locations or attempts to access a large number of different user IDs in a short time, this may indicate token abuse.
middleBrick's continuous monitoring in the Pro plan automatically scans your APIs on a configurable schedule, alerting you when Broken Access Control vulnerabilities are detected. This is particularly valuable because APIs evolve over time, and new endpoints may inadvertently introduce authorization bypasses.
Bearer Tokens-Specific Remediation
Remediating Broken Access Control in Bearer Token systems requires a defense-in-depth approach that combines proper token design, rigorous authorization checks, and secure implementation patterns.
1. Implement proper resource ownership validation:
function validateResourceOwnership(req, res, next) {
const { resourceId } = req.params;
const authenticatedUserId = req.user.id;
// Verify the resource belongs to the authenticated user
Resource.findById(resourceId)
.then(resource => {
if (!resource || resource.ownerId !== authenticatedUserId) {
return res.status(403).json({ error: 'Access denied' });
}
next();
})
.catch(next);
}
// Usage
app.get('/api/resources/:resourceId', authenticateToken, validateResourceOwnership, (req, res) => {
res.json(req.resource); // already validated
});
2. Use scopes and permissions effectively:
function checkScope(requiredScope) {
return (req, res, next) => {
const tokenScopes = req.user.scope || [];
if (!tokenScopes.includes(requiredScope)) {
return res.status(403).json({ error: 'Insufficient scope' });
}
next();
};
}
// Protect admin endpoints
app.post('/api/admin/users', authenticateToken, checkScope('admin'), (req, res) => {
// Only users with 'admin' scope can access
createAdminUser(req.body).then(user => res.status(201).json(user));
});
3. Implement token binding: Bind tokens to specific client characteristics to prevent token theft abuse.
function bindTokenToClient(req, res, next) {
const userAgent = req.get('User-Agent');
const ipAddress = req.ip;
// Store client info in token payload during issuance
const tokenPayload = {
sub: userId,
client: { userAgent, ipAddress },
exp: calculateExpiration()
};
// On each request, verify client hasn't changed
if (req.user.client.userAgent !== userAgent ||
req.user.client.ipAddress !== ipAddress) {
return res.status(401).json({ error: 'Token bound to different client' });
}
next();
}
4. Use refresh tokens with proper rotation:
// Issue short-lived access tokens with longer-lived refresh tokens
const accessToken = jwt.sign(
{ sub: userId, scope: ['read', 'write'] },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Rotate refresh tokens on each use (sliding window)
app.post('/refresh', (req, res) => {
const { token } = req.body;
// Verify refresh token
const user = verifyRefreshToken(token);
// Issue new refresh token and revoke old one
const newRefreshToken = generateRefreshToken(user.id);
storeRefreshToken(user.id, newRefreshToken);
const newAccessToken = generateAccessToken(user.id);
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
});
5. Implement proper error handling: Never reveal whether a resource exists or if authorization failed.
app.get('/api/users/:userId', authenticateToken, (req, res) => {
const requestedUserId = req.params.userId;
const authenticatedUserId = req.user.id;
// Always query, regardless of ownership
User.findById(requestedUserId)
.then(user => {
if (!user) {
return res.status(404).json({ error: 'Not found' });
}
if (user.id !== authenticatedUserId) {
return res.status(404).json({ error: 'Not found' });
}
res.json(user);
})
.catch(next);
});
6. Use middleBrick for continuous validation: After implementing these fixes, use middleBrick's CLI to verify your remediation:
middlebrick scan https://api.example.com --category=bola --fail-below=B
This command scans for Broken Object Level Authorization vulnerabilities and fails if the security score drops below 'B', making it perfect for CI/CD pipelines where you want to prevent insecure code from being deployed.
Frequently Asked Questions
How can I tell if my Bearer Token implementation has Broken Access Control vulnerabilities?
Look for APIs that accept user IDs or resource identifiers in URLs or request bodies without verifying that the authenticated user owns those resources. Common signs include endpoints like /api/users/{id}/profile or /api/documents/{docId} that return data without checking token ownership. You can also use middleBrick to scan your API endpoints - it specifically tests for BOLA (Broken Object Level Authorization) vulnerabilities by attempting to access resources across different user contexts without proper authorization.
What's the difference between authentication and authorization in Bearer Token security?
Authentication verifies who you are (proving your identity with a valid token), while authorization determines what you're allowed to do (checking permissions and resource ownership). A common Broken Access Control mistake is implementing authentication middleware that verifies tokens exist, but failing to implement authorization checks that verify the token holder has permission to access the specific resource they're requesting. Always validate both: first authenticate the token, then authorize the specific action against the requested resource.