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

Published Oct 8, 2026
·
Updated

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 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:

js 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:

js 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

js 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: js 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:

js 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:

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: ridingsa@gmail.com

Other sources

fast-jwt provides fast JSON Web Token (JWT) implementation. Prior to 6.3.0, fast-jwt createVerifier accepts Infinity for clockTolerance because its option validation checks type and negativity but not finiteness. In validateClaimDateValue, infinite positive and negative modifiers make exp and nbf comparisons always pass, allowing expired or not-yet-active tokens to be accepted. The verifier cache also derives infinite bounds, so entries created under this configuration can remain valid until eviction. Exploitation requires an application administrator or equivalent configuration path to set clockTolerance to Infinity. This issue is fixed in version 6.3.0.

— MITRE

Affected Software

2 affected componentsFixes available
npm/fast-jwt<6.3.0
npm/fast-jwt<=6.2.4
6.3.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/fast-jwt to a version that resolves this vulnerability.

    Fixed in 6.3.0
  2. Upgrade

    Upgrade fast-jwt to a version that resolves this vulnerability.

    Fixed in 6.3.0
  3. Configuration

    Cap clockTolerance to a reasonable upper bound of 300000 ms (5 minutes) and emit a process warning when the configured value exceeds that limit.

    fast-jwt verifier clockTolerance = maximum 300000 ms

Event History

Oct 8, 2026
CVE Published
via MITRE·09:45 PM
Data Sourced
via MITRE·09:45 PM
DescriptionSeverityWeakness
Advisory Published
via GitHub·10:02 PM
Data Sourced
via GitHub·10:02 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who can exploit this issue in practice?

Exploitation requires an application administrator or an equivalent configuration path to set the verifier's clockTolerance option to Infinity. It is not described as exploitable by an unauthenticated remote attacker acting alone.

2

Are applications using the default configuration affected?

The issue requires clockTolerance to be explicitly configured as Infinity. The provided information does not indicate that this is the default setting.

3

What is the impact of setting clockTolerance to Infinity?

Expired tokens and tokens whose nbf time is still in the future can both pass validation. Verifier-cache entries created with this configuration can also remain valid until eviction because their derived bounds are infinite.

4

What should be done if an immediate upgrade is not possible?

Do not configure clockTolerance as Infinity, and replace any such configuration with a finite value. Review verifier-cache behavior because entries created under the affected configuration may remain valid until eviction.

5

Which version fixes the issue?

The issue is fixed in fast-jwt version 6.3.0.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203