Race Condition with Jwt Tokens
How Race Condition Manifests in Jwt Tokens
Race conditions in JWT tokens typically occur during the token lifecycle operations—specifically during token creation, validation, and revocation. The most common manifestation happens when multiple concurrent requests attempt to modify a user's authentication state while relying on JWT tokens that are still valid.
Consider a logout scenario: a user clicks logout while another request is processing a payment. If the logout handler invalidates the token but the payment request validates the token before the invalidation takes effect, the system may process the payment as an authenticated user who should no longer have access.
// Vulnerable race condition pattern
const logout = async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
await invalidateToken(token); // Async operation
res.send('Logged out');
};
const processPayment = async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const isValid = await validateToken(token); // May succeed if logout not yet processed
if (isValid) {
await chargeCreditCard(req.body);
}
res.send('Payment processed');
};Another critical race condition occurs during token refresh operations. When a client requests a new token while the old token is still valid, concurrent requests might validate against different token states:
// Race condition in token refresh
const refreshToken = async (req, res) => {
const oldToken = req.headers.authorization.split(' ')[1];
const newToken = await generateNewToken();
await storeToken(newToken); // Async storage
// Meanwhile, another request validates oldToken
const isValid = await validateToken(oldToken);
if (isValid) {
await processSensitiveOperation();
}
};Token revocation lists (blacklists) introduce additional race condition vectors. If token revocation checks are eventually consistent or cached, a token might be validated successfully even after revocation initiation:
// Eventually consistent revocation race
const validateToken = async (token) => {
const isRevoked = await checkRevocationCache(token); // Cache may be stale
if (!isRevoked) {
return verifySignature(token);
}
return false;
};
const revokeToken = async (token) => {
await invalidateCache(token); // Cache update may not be immediate
await storeInRevocationList(token);
};Database-level race conditions also occur when token validation queries race against token state changes. Using non-transactional token validation against a database where token status changes asynchronously creates windows where invalid tokens appear valid:
// Database race condition
const validateToken = async (token) => {
const tokenRecord = await getTokenFromDatabase(token);
return tokenRecord && tokenRecord.validUntil > Date.now();
};
const invalidateToken = async (token) => {
await updateTokenStatus(token, 'invalid');
};Jwt Tokens-Specific Detection
Detecting race conditions in JWT token systems requires both static analysis of code patterns and dynamic testing of concurrent operations. MiddleBrick's scanner specifically targets JWT race condition vulnerabilities through black-box testing of token lifecycle operations.
MiddleBrick detects race conditions by simulating concurrent authentication state changes. The scanner creates multiple parallel requests that exercise token validation while simultaneously triggering token invalidation or state changes. This tests whether the system properly handles concurrent access to token validation logic.
Key detection patterns include:
- Concurrent logout and authenticated operation testing
- Simultaneous token refresh and validation attempts
- Parallel token revocation and usage operations
- Race condition detection during token rotation scenarios
The scanner examines JWT-specific endpoints for proper synchronization mechanisms. It looks for missing atomic operations around token validation, absence of database transactions around token state changes, and improper caching strategies that could lead to stale validation results.
MiddleBrick's JWT race condition detection includes testing for:
// What the scanner tests for
// 1. Missing atomic operations around token validation
// 2. Non-transactional database access during token operations
// 3. Eventually consistent revocation list implementations
// 4. Race conditions in token rotation/refresh flows
// 5. Missing synchronization in logout/clear-session operationsThe scanner also analyzes OpenAPI specifications for JWT-related endpoints, checking for proper rate limiting and authentication requirements that might mitigate race condition impacts. It specifically tests for token replay vulnerabilities where invalidated tokens might still be accepted due to timing issues.
MiddleBrick's LLM security module additionally checks for AI-specific JWT vulnerabilities, such as prompt injection attacks that could manipulate token validation logic or extract sensitive token information from system prompts.
Jwt Tokens-Specific Remediation
Remediating JWT race conditions requires implementing proper synchronization and atomic operations around token lifecycle management. The most effective approach is using database transactions combined with proper locking mechanisms.
For token validation and revocation, use atomic database operations with proper isolation levels:
// Atomic token validation with transactions
const validateToken = async (token) => {
const result = await db.transaction(async (trx) => {
const tokenRecord = await trx('tokens')
.where('token', token)
.forUpdate(); // Lock row for update
if (!tokenRecord || tokenRecord.validUntil < Date.now()) {
return false;
}
return verifySignature(token);
});
return result;
};
const invalidateToken = async (token) => {
await db.transaction(async (trx) => {
await trx('tokens')
.where('token', token)
.update({ status: 'invalid' });
});
};Implement proper token state management using a centralized token store with immediate consistency:
// Centralized token store with immediate consistency
class TokenStore {
constructor() {
this.store = new Map();
this.locks = new Map();
}
async validate(token) {
const lock = this.getLock(token);
await lock.acquire();
try {
const tokenData = this.store.get(token);
if (!tokenData || tokenData.expires < Date.now()) {
return false;
}
return verifySignature(token);
} finally {
lock.release();
}
}
async invalidate(token) {
const lock = this.getLock(token);
await lock.acquire();
try {
this.store.delete(token);
} finally {
lock.release();
}
}
getLock(token) {
if (!this.locks.has(token)) {
this.locks.set(token, new Semaphore(1));
}
return this.locks.get(token);
}
}Use atomic compare-and-swap operations for token refresh scenarios:
// Atomic token refresh
const refreshToken = async (oldToken) => {
const lockKey = `refresh_${oldToken}`;
const lock = this.getLock(lockKey);
await lock.acquire();
try {
const isValid = await this.validate(oldToken);
if (!isValid) {
throw new Error('Token invalid');
}
const newToken = await generateNewToken();
await this.storeNewToken(newToken);
// Atomic swap: invalidate old, activate new
await this.invalidate(oldToken);
await this.activate(newToken);
return newToken;
} finally {
lock.release();
}
};Implement proper error handling and retry logic for concurrent operations:
// Retry logic for concurrent token operations
const withRetry = async (operation, retries = 3) => {
for (let attempt = 0; attempt < retries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt)));
}
}
};
// Usage in logout
const safeLogout = async (req, res) => {
try {
await withRetry(async () => {
await invalidateToken(req.token);
});
res.send('Logged out');
} catch (error) {
res.status(500).send('Logout failed');
}
};