GitHub Advisoryadvisory · npm
GHSA-x937-hj6v-793p fast-jwt: Verifier cache accepts expired JWTs without iat.
MEDIUM
Official page ↗
- Aliases
- CVE-2026-107719
- Ecosystem
- npm
- Packages
- fast-jwt
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N
- Weaknesses
- CWE-613
- Reviewed
- 2026-10-08
Affected packages
• npm fast-jwt: < 6.3.4 → fixed in 6.3.4
Description
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 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.
Related
Weakness · not in database (1)
Alias · CVE (1)
- CVE-2026-107719 fast-jwt provides fast JSON Web Token (JWT) implementation. vulnerability