fast-jwt: Verifier cache accepts expired JWTs without iat.
### Summary `cacheSet` only derives its `exp` cache deadline inside `hasIat` (`src/verifier.js:127-140`). JWT `iat` is optional. For a valid token with `exp` but no `iat`, `cacheSet` substitutes `clockTimestamp + clockTolerance + cacheTTL` (`src/verifier.js:142-146`). Later, the cache path returns the saved payload before decoding or invoking `verifyToken` (`src/verifier.js:372-388`). Since expiration validation is in `verifyToken`, the expired token remains accepted until the cache deadline. ### Details The bug is an expiration-check bypass caused by caching. Normally, verification works like this: 1. Verify signature. 2. Check exp against the current time. 3. Return the JWT claims. With caching enabled, `fast-jwt` saves successful verification results so it can avoid repeating crypto work for the same token. The intended invariant is: > A cached token must stop being accepted at the same time as a non-cached token. But the cache computes its expiration incorrectly. In `fast-jwt/src/verifier.js:127`, the code first checks whether the payload has an iat (“issued at”) claim: ```js const hasIat = payload && typeof payload.iat === 'number' if (hasIat) { if (!ignoreExpiration && typeof payload.exp === 'number') { cacheValue[2] = payload.exp * 1000 + clockTolerance } } ``` That means `exp` is considered only when `iat` exists. However, `iat` is optional in JWT. A perfectly valid JWT may contain: ```js { "sub": "alice", "exp": 1700000001 } ``` with no iat. For that token, the expiration-based cache deadline is never set. The code falls back to the generic cache TTL—10 minutes by default: ```js const maxTTL = clockTimestamp + clockTolerance + cacheTTL cacheValue[2] = cacheValue[2] === 0 ? maxTTL : Math.min(cacheValue[2], maxTTL) ``` Later, cache lookup happens before decoding and expiration validation: ```js const [value, min, max] = cache.get(cacheKeyBuilder(token)) || [undefined, 0, 0] if (typeof value !== 'undefined' && (max === 0 || now <= max)) { return handleCachedResult(value, callback, promise) } ``` So the timeline is: ``` 12:00:00 Token has exp = 12:00:01, no iat. 12:00:00 Server verifies it successfully and caches its claims. 12:00:01 Token expires. 12:00:02 Attacker replays the identical token. 12:00:02 Cache hit returns old claims; exp is never rechecked. 12:10:00 Cache TTL finally ends; normal expiration rejection resumes. ``` An attacker cannot forge a token through this issue. They need a valid token first; typically their own, or a stolen bearer token. But they can keep using it after its intended expiration if all of these are true: - The application enables `cache`. - The JWT has `exp` but no `iat`. - The token was cached before expiry. - It is replayed before the cache entry expires. Impact depends on the application. For a short-lived access token, it can extend access by up to the configured `cacheTTL` of 10 minutes by default, or more if the application configured it that way. This undermines expiry as an authentication/session boundary. The minimal repair is to calculate the cache deadline from `exp` independently of `iat`. `iat` should only matter for `maxAge`, because max age is inherently defined relative to issuance time. ### PoC ```js const assert = require('node:assert/strict') const { createSigner, createVerifier } = require('fast-jwt') const key = 'audit-only-secret' const originalNow = Date.now try { const initialNow = 1_700_000_000_000 const exp = Math.floor(initialNow / 1000) + 1 // expires one second later // noTimestamp intentionally omits iat, while exp is retained. const sign = createSigner({ key, algorithm: 'HS256', noTimestamp: true }) const token = sign({ sub: 'alice', exp }) const verify = createVerifier({ key, algorithms: ['HS256'], cache: true, cacheTTL: 60_000 }) Date.now = () => initialNow assert.equal(verify(token).sub, 'alice') // Valid; stores cache entry. Date.now = () => exp * 1000 + 1 assert.equal(verify(token).sub, 'alice') // BUG: should throw FAST_JWT_EXPIRED. console.log('VULNERABLE: expired exp-without-iat token served from cache') } finally { Date.now = originalNow } ``` ### Impact This is an authentication/session-expiration bypass caused by incorrect cache validation. The following are impacted: - Applications using the affected fast-jwt code with cache: true. - Applications that rely on JWT exp to end sessions or limit bearer-token lifetime. - Tokens with exp but no iat, after they were successfully verified and cached.
beschreibt (1)
Zugehörige CVEs.
| ID | Schweregrad | ausnutzbar | ||
|---|---|---|---|---|
| CVE-2026-107719US | — | — | ||
fast-jwt provides fast JSON Web Token (JWT) implementation. Prior to 6.3.4, the fast-jwt createVerifier cache can continue accepting a previously valid, signed JWT after its exp ti… | ||||
Referenzen
Externe Belege — Patches, Meldungen, Analysen.
Advisories wie diese benennen die exponierten Produkte, die Angreifer auf Ihrer Angriffsfläche ins Visier nehmen. Sehen Sie mit einem kostenlosen Passiv-Scan, was exponiert ist.
Kostenloser Scan