-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Bug: clockTolerance accepts arbitrarily large values, bypassing exp verification entirely #1021
Description
Summary
The clockTolerance option in verify() accepts any positive integer with no upper bound validation. Passing Number.MAX_SAFE_INTEGER (or any value large enough that exp + clockTolerance overflows to Infinity) causes the expiry check to silently pass for any expired token, regardless of how long ago it expired.
Environment
jsonwebtokenversion: 9.0.3 (latest)- Node.js: v20+
Reproduction
const jwt = require("jsonwebtoken");
const SECRET = "supersecret";
// Sign a token that expired 1 year ago
const expiredToken = jwt.sign(
{ sub: "user", role: "admin" },
SECRET,
{ expiresIn: "-365d" }
);
// Normal verify correctly rejects it
try {
jwt.verify(expiredToken, SECRET);
} catch (e) {
console.log(e.message); // "jwt expired"
}
// Bypass with MAX_SAFE_INTEGER clockTolerance
const payload = jwt.verify(expiredToken, SECRET, {
clockTolerance: Number.MAX_SAFE_INTEGER // 9007199254740991
});
console.log(payload); // { sub: "user", role: "admin", ... } — token accepted!Root Cause
In verify.js, the expiry check is:
if (clockTimestamp >= payload.exp + (options.clockTolerance || 0)) {
return done(new TokenExpiredError(...));
}When clockTolerance is Number.MAX_SAFE_INTEGER, the addition payload.exp + 9007199254740991 produces a value far larger than any realistic clockTimestamp. The comparison becomes:
1775002429 >= 9007200998207420 → false
So the expiry check is skipped entirely. The same issue affects the nbf (not before) check via the same pattern.
No validation is performed on clockTolerance beyond checking it is a number:
// Current validation (insufficient):
if (options.clockTimestamp && typeof options.clockTimestamp !== "number") {
return done(new JsonWebTokenError("clockTimestamp must be a number"));
}
// clockTolerance has NO validation at allImpact
Any application that:
- Reads
clockTolerancefrom user input, a config file, environment variable, or a database without strict validation, OR - Has a dependency that passes an unvalidated
clockTolerance
...is vulnerable to complete expiry bypass. An attacker who can influence the clockTolerance value can reuse tokens that expired days, months, or years ago.
This is particularly dangerous in multi-tenant systems where token verification options may be partially user-controlled.
Suggested Fix
Add an upper bound to clockTolerance. A reasonable maximum is 300 seconds (5 minutes) or at most 86400 (1 day). For example:
if (options.clockTolerance !== undefined) {
if (typeof options.clockTolerance !== "number" || options.clockTolerance < 0) {
return done(new JsonWebTokenError("clockTolerance must be a non-negative number"));
}
if (options.clockTolerance > 300) {
return done(new JsonWebTokenError("clockTolerance must not exceed 300 seconds"));
}
}Alternatively, document clearly that clockTolerance must be a small value and add a warning when it exceeds a reasonable threshold.
Discovered via manual source code audit of v9.0.3.
Reported by Travis Burmaster — travis@burmaster.com