From b206ef4b92fec7c4238e1cf37a0d3cdb41f426c0 Mon Sep 17 00:00:00 2001 From: Abhishek Chauhan Date: Mon, 22 Jun 2026 10:03:00 -0500 Subject: [PATCH] fix(verify): validate clockTolerance is a non-negative number A non-number, NaN, Infinity, or negative `clockTolerance` silently corrupted the exp/nbf expiry checks. For example `clockTolerance: Infinity` makes `clockTimestamp >= exp + clockTolerance` always false, so an expired token is accepted with no error. Reject invalid values up front, mirroring the existing `clockTimestamp` validation. --- test/verify.tests.js | 55 ++++++++++++++++++++++++++++++++++++++++++++ verify.js | 5 ++++ 2 files changed, 60 insertions(+) diff --git a/test/verify.tests.js b/test/verify.tests.js index 88500756..3243e306 100644 --- a/test/verify.tests.js +++ b/test/verify.tests.js @@ -248,6 +248,61 @@ describe('verify', function() { }); }); + describe('option: clockTolerance', function () { + const clockTimestamp = 1000000000; + + it('should reject a non-number clockTolerance', function (done) { + const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + jwt.verify(token, key, {clockTolerance: '5'}, function (err, p) { + assert.equal(err.name, 'JsonWebTokenError'); + assert.equal(err.message, 'clockTolerance must be a non-negative number'); + assert.isUndefined(p); + done(); + }); + }); + + it('should reject a negative clockTolerance', function (done) { + const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + jwt.verify(token, key, {clockTolerance: -5}, function (err, p) { + assert.equal(err.name, 'JsonWebTokenError'); + assert.equal(err.message, 'clockTolerance must be a non-negative number'); + assert.isUndefined(p); + done(); + }); + }); + + it('should reject a non-finite clockTolerance', function (done) { + const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp + 1}, key); + jwt.verify(token, key, {clockTolerance: Infinity}, function (err, p) { + assert.equal(err.name, 'JsonWebTokenError'); + assert.equal(err.message, 'clockTolerance must be a non-negative number'); + assert.isUndefined(p); + done(); + }); + }); + + it('should reject an expired token instead of silently accepting it when clockTolerance is non-finite', function (done) { + // exp + Infinity === Infinity made `clockTimestamp >= exp + clockTolerance` + // always false, silently accepting an expired token. It is now rejected. + const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp - 1}, key); + jwt.verify(token, key, {clockTimestamp: clockTimestamp, clockTolerance: Infinity}, function (err, p) { + assert.equal(err.name, 'JsonWebTokenError'); + assert.equal(err.message, 'clockTolerance must be a non-negative number'); + assert.isUndefined(p); + done(); + }); + }); + + it('should accept a valid non-negative clockTolerance', function (done) { + const token = jwt.sign({foo: 'bar', iat: clockTimestamp, exp: clockTimestamp - 1}, key); + jwt.verify(token, key, {clockTimestamp: clockTimestamp, clockTolerance: 5}, function (err, p) { + assert.isNull(err); + assert.equal(p.foo, 'bar'); + done(); + }); + }); + }); + describe('option: maxAge and clockTimestamp', function () { // { foo: 'bar', iat: 1437018582, exp: 1437018800 } exp = iat + 218s const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE0MzcwMTg1ODIsImV4cCI6MTQzNzAxODgwMH0.AVOsNC7TiT-XVSpCpkwB1240izzCIJ33Lp07gjnXVpA'; diff --git a/verify.js b/verify.js index cdbfdc45..d46982f3 100644 --- a/verify.js +++ b/verify.js @@ -46,6 +46,11 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { return done(new JsonWebTokenError('clockTimestamp must be a number')); } + if (options.clockTolerance !== undefined && + (typeof options.clockTolerance !== 'number' || !Number.isFinite(options.clockTolerance) || options.clockTolerance < 0)) { + return done(new JsonWebTokenError('clockTolerance must be a non-negative number')); + } + if (options.nonce !== undefined && (typeof options.nonce !== 'string' || options.nonce.trim() === '')) { return done(new JsonWebTokenError('nonce must be a non-empty string')); }