Race Condition in Express with Bearer Tokens
Race Condition in Express with Bearer Tokens — how this specific combination creates or exposes the vulnerability
A race condition in an Express API that uses Bearer Tokens typically occurs when token validity checks and state-changing operations are not performed atomically. Because Bearer Tokens are often validated on a per-request basis without server-side session state, concurrent or out-of-order operations can expose timing windows where authorization decisions become inconsistent.
Consider an Express route that first verifies a Bearer Token and then performs an action such as updating or deleting a resource. If another request modifies the same resource or the token’s validity between the check and the action, the original authorization decision may no longer be valid. For example, an attacker could make two parallel requests: one that invalidates or rotates the token and another that relies on the still-accepted token to perform a privileged operation. Because token validation is often a lightweight middleware step, the window between verification and resource mutation can allow unauthorized changes.
In a more specific scenario, an endpoint that accepts an identifier from the client and performs a database update can be vulnerable if the token check does not tightly scope the resource being modified. An attacker might issue multiple requests with the same Bearer Token but different identifiers, and due to nondeterministic execution order, one request may see an updated state while another does not. This can lead to unauthorized access across user boundaries, effectively turning a missing authorization check into a bypass via timing and concurrency.
These issues are compounded when token revocation or state changes happen asynchronously, such as logout or password reset mechanisms that invalidate tokens in a database or cache. An in-flight request that passed validation before the invalidation may still proceed, because middleware does not re-check token state mid-request. The combination of stateless Bearer Token usage and non-atomic authorization + mutation creates a race condition that is difficult to detect without targeted concurrency testing and precise correlation between authentication and authorization logic.
Real-world attack patterns parallel known issues in OAuth token handling and IDOR, where timing and ordering enable elevation of privilege. Although this is not directly tied to a specific CVE in every case, the pattern maps to OWASP API Top 10 2023 A01:2023 — Broken Object Level Authorization, and can be detected by scanning tools that correlate authentication mechanisms with authorization checks across endpoints.
Bearer Tokens-Specific Remediation in Express — concrete code fixes
Remediation focuses on ensuring that token validation and resource operations are treated as a single, consistent unit. You should avoid checking a Bearer Token in one middleware layer and then performing sensitive operations in another without re-verifying intent and scope. The following patterns demonstrate secure handling in Express.
1. Atomic validation and operation in a single handler
Keep token validation and resource mutation together so that no state can change between them. Use a middleware that attaches verified claims to the request and immediately use them in the route logic.
import express from 'express';
import jwt from 'jsonwebtoken';
const app = express();
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // attach claims for downstream use
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
app.delete('/resources/:id', authenticate, (req, res) => {
const resourceId = req.params.id;
const userId = req.user.sub;
// Perform the delete in a single logical unit; in practice, use a transaction or equivalent
const result = db.run('DELETE FROM resources WHERE id = ? AND owner_id = ?', [resourceId, userId]);
if (result.changes === 0) {
return res.status(404).json({ error: 'Not found or insufficient permissions' });
}
res.status(204).end();
});
2. Use parameterized queries and ownership checks to avoid inconsistent state
Ensure that any operation includes the user identity as part of the filter, so even if a race condition occurs, the database enforces ownership. This approach mitigates timing-based authorization bypasses.
app.put('/resources/:id', authenticate, async (req, res) => {
const { id } = req.params;
const { data } = req.body;
const userId = req.user.sub;
// Use a parameterized query that includes owner_id in the WHERE clause
const sql = 'UPDATE resources SET data = ? WHERE id = ? AND owner_id = ?';
const result = await db.run(sql, [data, id, userId]);
if (result.changes === 0) {
return res.status(403).json({ error: 'Forbidden: resource does not belong to you' });
}
res.json({ id, data });
});
3. Avoid token state mutation during request processing
Do not allow concurrent requests to mutate the same token’s state (e.g., revocation or rotation) in a way that in-flight requests can observe partial updates. If token invalidation is required, prefer short-lived tokens and refresh workflows rather than mutating validity mid-request.
// Example: short-lived token verification without in-flight invalidation checks
app.get('/profile', authenticate, (req, res) => {
// No additional revocation check inside the handler; rely on short expiry and secure storage
res.json({ sub: req.user.sub, name: req.user.name });
});
4. Prefer scoped tokens and claims-based authorization
Include scope and resource identifiers in the token claims so that authorization decisions can be made without additional lookups that might be affected by race conditions. Validate these claims within the same handler.
// Token payload includes { sub, scope: 'resources:write', resource_id: '123' }
function validateScope(req, res, next) {
if (req.user.scope !== 'resources:write' || req.user.resource_id !== req.params.id) {
return res.status(403).json({ error: 'Insufficient scope' });
}
next();
}
app.post('/resources/:id/action', authenticate, validateScope, (req, res) => {
// Proceed with action
res.json({ ok: true });
});
5. Use robust error handling and consistent response codes
Ensure that failures to validate tokens or enforce ownership return consistent, non-informative error messages to reduce information leakage that could aid an attacker in refining race-based attempts.
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Unauthorized' });
}
res.status(500).json({ error: 'Internal server error' });
});