Skip to content

Bug: clockTolerance accepts arbitrarily large values, bypassing exp verification entirely #1021

@travis-burmaster

Description

@travis-burmaster

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

  • jsonwebtoken version: 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 all

Impact

Any application that:

  1. Reads clockTolerance from user input, a config file, environment variable, or a database without strict validation, OR
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions