Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
MEDIUM severity

CVE-2026-107721 — fast-jwt

MEDIUMFix: nearform/fast-jwt@c0fb5b8

CVE-2026-107721 is a medium-severity (CVSS 5.9) CWE-613 vulnerability in fast-jwt. A fix is available for fast-jwt — see the affected versions and patch details below.

fast-jwt clockTolerance: Infinity silently bypasses both exp and nbf validation (and persists in the verifier cache)

Also known asGHSA-687g-22h4-j4w4
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 21, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • A successful exploit gives an attacker total control of the affected component, not partial access.
  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-107721.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs26th percentile — riskier than 26% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-107721 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,415 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

89other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
fast-jwtnpm
789Kdownloads / week

Description

Summary

createVerifier({ clockTolerance: Infinity }) silently bypasses both exp (expiry) AND nbf (not-before) validation. Any expired or not-yet-active token is accepted as valid. The same primitive also corrupts the verifier's internal cache so cached entries inherit infinite validity — they remain valid past a later developer-removed Infinity config until LRU eviction.

Vulnerable code

src/verifier.js:531-533 — option validation only rejects negative values, not Infinity:

if (clockTolerance && (typeof clockTolerance !== 'number' || clockTolerance < 0)) {
  throw new TokenError(TokenError.codes.invalidOption, 'The clockTolerance option must be a positive number.')
}

Infinity passes (it's a number, not less than 0, truthy).

src/verifier.js:583-602 — clockTolerance flows into the date-claim validators:

if (!ignoreNotBefore) {
  validators.push({ ..., modifier: -clockTolerance })   // → -Infinity
}
if (!ignoreExpiration) {
  validators.push({ ..., modifier: +clockTolerance })   // → Infinity
}

src/verifier.js:198-205 — applies modifier additively, producing always-pass comparisons:

function validateClaimDateValue(value, modifier, now, greater, errorCode, errorVerb) {
  const adjusted = value * 1000 + (modifier || 0)   // → ±Infinity
  const valid = greater ? now >= adjusted : now <= adjusted   // → always true
  ...
}

Empirical PoC

const { createSigner, createVerifier } = require('fast-jwt')
const secret = 'test-secret-with-enough-length-to-pass'

const sign = createSigner({ key: secret, algorithm: 'HS256' })
const expiredToken = sign({
  sub: 'alice',
  iat: Math.floor(Date.now()/1000) - 3600,
  exp: Math.floor(Date.now()/1000) - 1800,  // expired 30 min ago
})

const v1 = createVerifier({ key: secret })
try { v1(expiredToken) } catch (e) { console.log('baseline rejects:', e.message) }
// → "The token has expired at ..."

const v2 = createVerifier({ key: secret, clockTolerance: Infinity })
console.log('bypass:', v2(expiredToken))
// → { sub: 'alice', iat: ..., exp: ... }  ← expired token accepted as valid

// Not-yet-active token (nbf in 1 day) — same bypass
const futureToken = sign({
  sub: 'bob',
  iat: Math.floor(Date.now()/1000),
  nbf: Math.floor(Date.now()/1000) + 86400,
})
console.log('bypass nbf:', v2(futureToken))
// → { sub: 'bob', ... }  ← not-yet-active token accepted as valid

Verified against fast-jwt@HEAD on 2026-06-03 (commit pulled today).

Cache side-effect

The verifier's LRU cache uses clockTolerance to compute the cache entry's expiry window:

src/verifier.js:121-134:

cacheValue[1] = ... payload.nbf * 1000 - clockTolerance : 0                   // → -Infinity
cacheValue[2] = payload.exp * 1000 + clockTolerance                            // → Infinity
const maxTTL = clockTimestamp + clockTolerance + cacheTTL                      // → Infinity

With clockTolerance: Infinity, cache entries are stored with [min=-Infinity, max=Infinity]. The cache-hit check (min === 0 || now < min || now <= max) always passes for cached tokens.

Consequence: a developer who briefly sets clockTolerance: Infinity (e.g., during debug) and then removes it will find that any verifications performed during the debug window remain cached as valid until LRU eviction (default 1000 entries).

Asymmetric hardening — the smoking-gun shape

src/signer.js:98, 104 CORRECTLY uses Number.isFinite() to reject Infinity for expiresIn and notBefore:

expiresIn != null && Number.isFinite(expiresIn)
  ? Math.floor((iat + expiresIn) / 1000)
  : ...

The verifier's clockTolerance validation doesn't apply the same guard. The same < 0 check pattern is also applied to clockTimestamp (line 527-529) and cacheTTL (line 535-537) — both also accept Infinity.

This is the kind of asymmetry that often indicates a missed hardening pass: the sign-side was hardened against Infinity but the verify-side wasn't.

Threat model

The bug requires the developer to (mis)configure clockTolerance: Infinity. Realistic ways this happens:

  1. Developer using Infinity as a sentinel for "disable expiry": common JS idiom; many libraries accept Infinity as "no limit." fast-jwt's signer treats Infinity as invalid (Number.isFinite false) but the verifier silently accepts it.
  2. JSON / env-var misconfig: config file or env var sets clockTolerance to "Infinity" (string); Number("Infinity") === Infinity. Surprising via JSON.parse + Number cast or even JSON.parse('{"clockTolerance": null}') if the codec accepts null → Infinity.
  3. Test config bleed: integration tests use Infinity to make tokens never expire during long-running tests; the config bleeds into production.

For a JWT library, silently disabling token expiry on a "looks like a non-negative number" input is a security boundary failure.

Suggested fix

Single-line addition: use Number.isFinite() consistent with signer.js:

if (clockTolerance && (typeof clockTolerance !== 'number' || !Number.isFinite(clockTolerance) || clockTolerance < 0)) {
  throw new TokenError(TokenError.codes.invalidOption, 'The clockTolerance option must be a finite, non-negative number.')
}

Same fix for clockTimestamp (line 527-529) and cacheTTL (line 535-537) for consistency.

Optional defense-in-depth: cap clockTolerance to a reasonable upper bound (e.g., 5 minutes = 300000 ms) with a process.emitWarning above that. Most legitimate use cases need <60s tolerance.

Affected versions

All versions since clockTolerance was first introduced in PR #193 (v1.5.1). Current main HEAD on 2026-06-03 is affected.

Reporter

Andrew Ridings (independent security researcher). Happy to coordinate disclosure timing and follow up with any clarifications. Email: [email protected]

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmfast-jwtall versions6.3.0npm install fast-jwt@6.3.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for fast-jwt, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update fast-jwt to 6.3.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-107721 is resolved across your whole dependency graph.

  3. Workarounds

    Put an independent control in front of the weakness: restrict the affected endpoint or interface to trusted networks, require an additional authentication factor or proxy-level check, and invalidate existing sessions and credentials in case the flaw has already been used.

Frequently Asked Questions

## Summary `createVerifier({ clockTolerance: Infinity })` silently bypasses both `exp` (expiry) AND `nbf` (not-before) validation. Any expired or not-yet-active token is accepted as valid. The same primitive also corrupts the verifier's internal cache so cached entries inherit infinite validity — they remain valid past a later developer-removed Infinity config until LRU eviction. ## Vulnerable code `src/verifier.js:531-533` — option validation only rejects negative values, not Infinity: ```js if (clockTolerance && (typeof clockTolerance !== 'number' || clockTolerance < 0)) { throw new To
O3 Security · Impact-Aware SCA

Is CVE-2026-107721 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-107721: fast-jwt — Fixed in 6.3.0