HIGH race conditionexpressbearer tokens

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' });
});

Frequently Asked Questions

Can race conditions with Bearer Tokens be detected by scanning an API?
Yes, scanners can correlate authentication mechanisms with authorization checks and highlight endpoints where token validation and resource mutation are not performed atomically, indicating potential race conditions.
How often should I rotate Bearer Tokens to reduce race condition risk?
Rotation alone does not fix race conditions; focus on making authorization checks atomic and scoping tokens to specific resources. Use short-lived tokens where feasible and ensure revocation checks do not introduce inconsistent state.