From 4f3a4c9a2b157facc47ef8b65e1af844296a3b85 Mon Sep 17 00:00:00 2001 From: ArockiaRajamanickam Date: Sun, 2 Aug 2026 11:53:00 +0530 Subject: [PATCH] fix(verify): reject tokens with an unsupported "crit" header parameter RFC 7515 Section 4.1.11 says that when the JOSE header carries a "crit" member listing extension header parameters, those parameters MUST be understood and processed, and that if any of them are not understood and supported by the recipient then the JWS is invalid. This library implements no "crit" extension, but jwt.verify ignored the member entirely and returned the payload as though nothing had been marked critical. The result is a cross-implementation interpretation split rather than a signature bypass: the sharpest case is RFC 7797, where b64:false with crit:["b64"] means the payload segment is not base64url encoded, so a conformant verifier reads a different payload out of the same signed token than this library does. jwt.verify now rejects any token that carries a "crit" header. Because the library supports zero crit extensions, this removes no capability it ever offered; it turns silent misinterpretation into an explicit error. Fixes #1032 --- README.md | 1 + test/header-crit.test.js | 85 ++++++++++++++++++++++++++++++++++++++++ verify.js | 8 ++++ 3 files changed, 94 insertions(+) create mode 100644 test/header-crit.test.js diff --git a/README.md b/README.md index 4e20dd9c..6c135870 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,7 @@ Error object: * message: * 'invalid token' - the header or payload could not be parsed * 'jwt malformed' - the token does not have three components (delimited by a `.`) + * 'unsupported "crit" header parameter' - the token marks header parameters as critical ([RFC 7515 Section 4.1.11](https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.11)), which this library does not implement * 'jwt signature is required' * 'invalid signature' * 'jwt audience invalid. expected: [OPTIONS AUDIENCE]' diff --git a/test/header-crit.test.js b/test/header-crit.test.js new file mode 100644 index 00000000..a67b1ed7 --- /dev/null +++ b/test/header-crit.test.js @@ -0,0 +1,85 @@ +'use strict'; + +const jwt = require('../'); +const expect = require('chai').expect; +const util = require('util'); +const testUtils = require('./test-utils'); + +function signWithCrit(crit, extraHeader) { + const header = Object.assign({}, extraHeader); + if (crit !== undefined) { + header.crit = crit; + } + return jwt.sign({sub: 'foo'}, 'secret', {algorithm: 'HS256', header}); +} + +describe('crit', function () { + describe('`jwt.verify` with a "crit" header parameter', function () { + [ + // an extension nobody implements + ['http://example.invalid/UNDEFINED'], + // RFC 7797 unencoded payload: a conformant verifier reads a different payload + ['b64'], + // names the producer is not even allowed to mark critical + ['alg'], + // shapes RFC 7515 forbids producers from emitting + [], + 'b64', + 1, + null, + {}, + ].forEach((crit) => { + it(`should error with value ${util.inspect(crit)}`, function (done) { + const token = signWithCrit(crit, {'http://example.invalid/UNDEFINED': true, b64: false}); + testUtils.verifyJWTHelper(token, 'secret', {}, (err) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'unsupported "crit" header parameter'); + }); + }); + }); + }); + + it('should error before the "complete" option can expose the payload', function (done) { + const token = signWithCrit(['http://example.invalid/UNDEFINED']); + testUtils.verifyJWTHelper(token, 'secret', {complete: true}, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.instanceOf(jwt.JsonWebTokenError); + expect(err).to.have.property('message', 'unsupported "crit" header parameter'); + expect(decoded).to.be.undefined; + }); + }); + }); + }); + + describe('`jwt.verify` without a "crit" header parameter', function () { + it('should verify a token that has no "crit" header', function (done) { + const token = signWithCrit(undefined); + testUtils.verifyJWTHelper(token, 'secret', {}, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('sub', 'foo'); + }); + }); + }); + + it('should verify a token with unrecognized headers that are not marked critical', function (done) { + const token = signWithCrit(undefined, {'http://example.invalid/UNDEFINED': true}); + testUtils.verifyJWTHelper(token, 'secret', {}, (err, decoded) => { + testUtils.asyncCheck(done, () => { + expect(err).to.be.null; + expect(decoded).to.have.property('sub', 'foo'); + }); + }); + }); + }); + + describe('`jwt.decode`', function () { + it('should still decode a token with a "crit" header, as it does not verify', function () { + const token = signWithCrit(['http://example.invalid/UNDEFINED']); + const decoded = jwt.decode(token, {complete: true}); + expect(decoded.header).to.have.deep.property('crit', ['http://example.invalid/UNDEFINED']); + expect(decoded.payload).to.have.property('sub', 'foo'); + }); + }); +}); diff --git a/verify.js b/verify.js index cdbfdc45..50ca8fab 100644 --- a/verify.js +++ b/verify.js @@ -83,6 +83,14 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) { } const header = decodedToken.header; + + //RFC 7515 Section 4.1.11: "crit" lists extension header parameters that must be + //understood and processed, and the JWS is invalid if any of them are not. This + //library implements no such extension, so any "crit" header is unsupported. + if (typeof header.crit !== 'undefined') { + return done(new JsonWebTokenError('unsupported "crit" header parameter')); + } + let getSecret; if(typeof secretOrPublicKey === 'function') {