Bola Idor with Jwt Tokens
How BOLA/IDOR Manifests in JWT Tokens
Broken Object Level Authorization (BOLA), also known as Insecure Direct Object Reference (IDOR), occurs when an application fails to properly verify whether a user has permission to access specific objects. JWT tokens, while excellent for stateless authentication, can introduce unique BOLA vulnerabilities when object identifiers are embedded in token claims without proper authorization checks.
The most common JWT BOLA pattern involves user IDs or object IDs stored in token claims. Consider this flawed implementation:
// Vulnerable: trusting token claims without verification
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const userId = decoded.userId; // Trust this without validation
const postId = req.params.postId;
// NO authorization check!
const post = await getPostById(postId);
res.json(post);
The vulnerability here is that any authenticated user can modify their JWT token's userId claim to access any post. Since JWT tokens are cryptographically signed, users cannot forge tokens, but they can easily change the userId claim if the application doesn't verify it against the authenticated identity.
Another JWT-specific BOLA pattern involves role-based access embedded in tokens:
// Vulnerable: role escalation via token modification
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const role = decoded.role; // Admin? User? Trust without validation
// NO role verification!
const allUsers = await getAllUsers();
res.json(allUsers);
Attackers can modify their token's role claim from user to admin, gaining unauthorized access to administrative endpoints. The cryptographic signature prevents token forgery but doesn't prevent legitimate users from modifying claims if the application doesn't validate them.
Database ID exposure through JWT claims creates another attack vector:
// Vulnerable: exposing sequential IDs in tokens
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const accountId = decoded.accountId; // Sequential ID exposure
// NO authorization check!
const account = await getAccountById(accountId);
res.json(account);
Even with proper token signing, sequential IDs allow attackers to enumerate and access other users' accounts by simply incrementing the ID in their token.
JWT-Specific Detection Methods
Detecting JWT-specific BOLA vulnerabilities requires examining both the token structure and the authorization logic. Here's how to identify these issues in your JWT implementation:
1. Token Claim Analysis
Examine which claims your application trusts without validation. Common problematic claims include:
// Scan for vulnerable patterns
const fs = require('fs');
const code = fs.readFileSync('server.js', 'utf8');
// Detect claims used without validation
const vulnerablePatterns = [
/jwt\.verify\(.*\).*[\n\r].*const (userId|role|accountId|ownerId) = decoded\.[a-zA-Z]+;/g,
/decoded\.[a-zA-Z]+.*=.*req\.(params|query|body)/g
];
vulnerablePatterns.forEach(pattern => {
const matches = code.match(pattern);
if (matches) {
console.log('Potential BOLA vulnerability:', matches);
}
});
2. Authorization Logic Verification
Check if your endpoints verify that the authenticated user matches the resource owner:
// Test for missing authorization checks
function hasAuthorizationCheck(code) {
const patterns = [
/const.*=.*decoded\..*;/,
/const.*=.*req\.(params|query|body)/,
/await.*get.*ById.*\(.*\)/
];
// Look for missing ownership verification
const missingCheck = code.match(
new RegExp(
`${patterns[0].source}.*${patterns[2].source}.*res\.json`,
's'
)
);
return missingCheck;
}
3. Automated Scanning with middleBrick
middleBrick's black-box scanning approach can detect JWT BOLA vulnerabilities without requiring source code access:
# Scan JWT-protected endpoints
middlebrick scan https://api.example.com/user/123/posts \
--auth "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# middleBrick tests for:
# - Parameter manipulation (changing user IDs in JWT claims)
# - Role escalation attempts
# - Sequential ID enumeration
# - Missing authorization checks
The scanner automatically tests for BOLA by manipulating JWT claims and verifying whether the application properly enforces access controls. It checks if changing user IDs in tokens grants access to unauthorized resources.
4. Runtime Monitoring
Implement monitoring to detect BOLA attempts:
const winston = require('winston');
function monitorAuthorization(req, res, next) {
const start = Date.now();
const originalSend = res.send;
res.send = function(data) {
const duration = Date.now() - start;
// Log suspicious patterns
if (duration < 10 && req.method === 'GET') {
winston.warn({
message: 'Potential BOLA attempt',
endpoint: req.path,
userId: req.user?.id,
resourceId: req.params.id || req.body.id,
ip: req.ip
});
}
originalSend.call(this, data);
};
next();
}
JWT-Specific Remediation Techniques
Fixing JWT BOLA vulnerabilities requires implementing proper authorization checks and avoiding trust in token claims. Here are JWT-specific remediation strategies:
1. Never Trust Token Claims for Authorization
Always validate token claims against the actual authenticated user and resource ownership:
// Secure implementation
const jwt = require('jsonwebtoken');
async function secureEndpoint(req, res) {
try {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Verify token user matches the requested resource
const requestedUserId = req.params.userId || decoded.userId;
if (decoded.userId !== requestedUserId) {
return res.status(403).json({
error: 'Access denied - resource ownership mismatch'
});
}
// Fetch resource with proper ownership verification
const resource = await getResourceByOwner(decoded.userId, req.params.resourceId);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
res.json(resource);
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
2. Implement Database-Level Authorization
Use database queries that enforce ownership at the data layer:
// Secure database query with ownership verification
async function getPostByIdAndOwner(postId, ownerId) {
const query = `
SELECT * FROM posts
WHERE id = $1 AND owner_id = $2
AND deleted_at IS NULL
`;
const result = await db.query(query, [postId, ownerId]);
return result.rows[0];
}
// Usage
app.get('/posts/:postId', async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const post = await getPostByIdAndOwner(req.params.postId, decoded.userId);
if (!post) {
return res.status(404).json({ error: 'Post not found or access denied' });
}
res.json(post);
});
3. Use Non-Sequential Identifiers
Replace sequential IDs with UUIDs or other non-guessable identifiers:
// Generate UUIDs instead of sequential IDs
const { v4: uuidv4 } = require('uuid');
// Database schema
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
owner_id UUID REFERENCES users(id),
content TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
// Secure JWT handling
app.get('/posts/:postId', async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Parse UUID and validate format
const postId = req.params.postId;
if (!isValidUUID(postId)) {
return res.status(400).json({ error: 'Invalid post ID format' });
}
const post = await getPostByIdAndOwner(postId, decoded.userId);
if (!post) {
return res.status(404).json({ error: 'Post not found or access denied' });
}
res.json(post);
});
4. Implement Role-Based Access Control (RBAC) with Database Verification
Never trust role claims in JWT tokens. Verify roles against the database:
// Secure RBAC implementation
async function verifyRole(userId, requiredRole) {
const result = await db.query(
'SELECT role FROM users WHERE id = $1',
[userId]
);
if (!result.rows[0]) {
return false;
}
const userRole = result.rows[0].role;
const roleHierarchy = { user: 1, admin: 2, superadmin: 3 };
return roleHierarchy[userRole] >= roleHierarchy[requiredRole];
}
app.get('/admin/users', async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const hasAccess = await verifyRole(decoded.userId, 'admin');
if (!hasAccess) {
return res.status(403).json({ error: 'Admin access required' });
}
const users = await db.query('SELECT id, email, role FROM users');
res.json(users.rows);
});
5. Use Short-Lived Tokens with Refresh Mechanism
Implement short-lived JWT tokens with refresh tokens to reduce the window of opportunity for BOLA attacks:
// Secure token generation
const generateTokens = (userId) => {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
};
// Secure refresh endpoint
app.post('/refresh', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.REFRESH_SECRET);
// Verify refresh token belongs to user
const user = await getUserById(decoded.userId);
if (!user || user.refresh_token !== token) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
const newTokens = generateTokens(decoded.userId);
res.json(newTokens);
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
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 |