Time Of Check Time Of Use with Jwt Tokens
How Time Of Check Time Of Use Manifests in JWT Tokens
A Time‑Of‑Check Time‑Of‑Use (TOCTOU) race condition appears when an application validates a JWT in one step and later uses the token (or its claims) after an attacker has had a chance to modify it. Because JWTs are stateless, the window between validation and use can be exploited if the code does not treat verification and consumption as an atomic operation.
- Signature verification after claim usage. Some frameworks decode the token first (e.g.,
jwt.decode(token)) to read claims such asroleorsub, then later calljwt.verify(token, secret). If an attacker can replace the token between those two calls, the application may act on a forged claim while still believing the token is valid. - Time‑based claims checked separately. An endpoint might first check
exp(expiration) against the current time, then retrieve a user record from a database, and finally authorize an action using thesubclaim. An attacker who can delay the request (e.g., via network throttling) can cause the expiration check to pass while the token is later replayed after it has actually expired. - Audience or issuer validation after resource access. Code may fetch a resource using the
subclaim as a key, then later verify that the token’saudmatches the expected service. If the token is swapped after the resource fetch but before the audience check, the attacker can gain access to data intended for another service.
These patterns map directly to OWASP API Security Top 10 2023 item API1: Broken Object Level Authorization, because the authorization decision relies on data that can be altered between the moment it is checked and the moment it is enforced.
JWT Tokens-Specific Detection
Detecting TOCTOU in JWT handling requires looking for places where token validation is split across multiple code paths. Static analysis can spot calls to a decode‑only function followed later by a verify call, but black‑box scanners like middleBrick can also reveal the issue by observing how the API behaves when presented with a token that has been altered after an initial check.
middleBrick’s unauthenticated black‑box scan sends a series of crafted requests to each endpoint that accepts an Authorization header bearing a JWT. It:
- Baselines the response with a valid token.
- Replays the same token after a short delay to see if the server still accepts it (checking for missing expiration enforcement).
- Flips a single claim (e.g., changes
rolefrom "user" to "admin") and resends the token immediately after the server has performed an early validation step (such as checkingexp) to see if the altered claim is acted upon. - Analyzes the OpenAPI/Swagger specification (if available) to identify endpoints that list a JWT security scheme and then cross‑references the runtime findings to flag any endpoint where validation appears to be incomplete.
For example, scanning https://api.example.com/orders with the CLI:
middlebrick scan https://api.example.com/orders
If the scanner detects that a token with an altered role claim is accepted after an early expiration check, it will report a finding under the “Authentication” category with severity “Medium” and include the exact request/response pairs that demonstrate the race condition.
Because middleBrick does not require agents, credentials, or configuration, developers can run this scan locally, in CI pipelines, or directly from an IDE via the MCP Server integration to catch TOCTOU issues before they reach production.
JWT Tokens-Specific Remediation
The fix is to ensure that token verification and consumption happen in a single, indivisible step. Most JWT libraries provide a verify function that returns the payload only after confirming the signature and, optionally, validating registered claims such as exp, nbf, aud, and iss. Using that function eliminates the window where an attacker can tamper with the token.
Below is a vulnerable Node.js example using the popular jsonwebtoken package:
const jwt = require('jsonwebtoken');
function getUserFromToken(token) {
// ❌ Vulnerable: decode first, verify later
const payload = jwt.decode(token); // no verification
if (payload.exp && payload.exp < Date.now() / 1000) {
throw new Error('Token expired');
}
// ... later, after some DB lookup ...
const verified = jwt.verify(token, process.env.JWT_SECRET); // verification happens here
return verified.sub;
}
An attacker could replace token after the jwt.decode call but before jwt.verify, causing the application to act on a forged sub while still believing the token passed the expiration check.
The corrected version performs verification first and uses the returned payload directly:
const jwt = require('jsonwebtoken');
function getUserFromToken(token) {
// ✅ Secure: verify and obtain payload in one step
const payload = jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'my-service',
audience: 'orders-api',
// expirationLeeway can be used to tolerate small clock skew
});
// payload now contains sub, role, etc., and is guaranteed to be valid
return payload.sub;
}
// Usage in an Express route
app.get('/orders', (req, res) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) return res.sendStatus(401);
const token = auth.slice(7);
try {
const userId = getUserFromToken(token);
// proceed with authorized logic using userId
res.json({ orders: getOrdersForUser(userId) });
} catch (err) {
res.sendStatus(401);
}
});
Additional hardening steps:
- Set short expiration times (
exp) and refresh tokens stored server‑side to limit the usefulness of any stolen token. - Include a
jti(JWT ID) claim and maintain a server‑side revocation list or use a nonce to prevent replay attacks. - When using libraries that separate decoding and verification (e.g., some language‑specific JWT implementations), always call the verify method first and only then extract claims.
By adopting this pattern, the TOCTOU window disappears, and the API’s authorization decision is based on a token that cannot be altered between check and use.