Skip to content

Commit 4f3a4c9

Browse files
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
1 parent b924272 commit 4f3a4c9

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ Error object:
308308
* message:
309309
* 'invalid token' - the header or payload could not be parsed
310310
* 'jwt malformed' - the token does not have three components (delimited by a `.`)
311+
* '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
311312
* 'jwt signature is required'
312313
* 'invalid signature'
313314
* 'jwt audience invalid. expected: [OPTIONS AUDIENCE]'

test/header-crit.test.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use strict';
2+
3+
const jwt = require('../');
4+
const expect = require('chai').expect;
5+
const util = require('util');
6+
const testUtils = require('./test-utils');
7+
8+
function signWithCrit(crit, extraHeader) {
9+
const header = Object.assign({}, extraHeader);
10+
if (crit !== undefined) {
11+
header.crit = crit;
12+
}
13+
return jwt.sign({sub: 'foo'}, 'secret', {algorithm: 'HS256', header});
14+
}
15+
16+
describe('crit', function () {
17+
describe('`jwt.verify` with a "crit" header parameter', function () {
18+
[
19+
// an extension nobody implements
20+
['http://example.invalid/UNDEFINED'],
21+
// RFC 7797 unencoded payload: a conformant verifier reads a different payload
22+
['b64'],
23+
// names the producer is not even allowed to mark critical
24+
['alg'],
25+
// shapes RFC 7515 forbids producers from emitting
26+
[],
27+
'b64',
28+
1,
29+
null,
30+
{},
31+
].forEach((crit) => {
32+
it(`should error with value ${util.inspect(crit)}`, function (done) {
33+
const token = signWithCrit(crit, {'http://example.invalid/UNDEFINED': true, b64: false});
34+
testUtils.verifyJWTHelper(token, 'secret', {}, (err) => {
35+
testUtils.asyncCheck(done, () => {
36+
expect(err).to.be.instanceOf(jwt.JsonWebTokenError);
37+
expect(err).to.have.property('message', 'unsupported "crit" header parameter');
38+
});
39+
});
40+
});
41+
});
42+
43+
it('should error before the "complete" option can expose the payload', function (done) {
44+
const token = signWithCrit(['http://example.invalid/UNDEFINED']);
45+
testUtils.verifyJWTHelper(token, 'secret', {complete: true}, (err, decoded) => {
46+
testUtils.asyncCheck(done, () => {
47+
expect(err).to.be.instanceOf(jwt.JsonWebTokenError);
48+
expect(err).to.have.property('message', 'unsupported "crit" header parameter');
49+
expect(decoded).to.be.undefined;
50+
});
51+
});
52+
});
53+
});
54+
55+
describe('`jwt.verify` without a "crit" header parameter', function () {
56+
it('should verify a token that has no "crit" header', function (done) {
57+
const token = signWithCrit(undefined);
58+
testUtils.verifyJWTHelper(token, 'secret', {}, (err, decoded) => {
59+
testUtils.asyncCheck(done, () => {
60+
expect(err).to.be.null;
61+
expect(decoded).to.have.property('sub', 'foo');
62+
});
63+
});
64+
});
65+
66+
it('should verify a token with unrecognized headers that are not marked critical', function (done) {
67+
const token = signWithCrit(undefined, {'http://example.invalid/UNDEFINED': true});
68+
testUtils.verifyJWTHelper(token, 'secret', {}, (err, decoded) => {
69+
testUtils.asyncCheck(done, () => {
70+
expect(err).to.be.null;
71+
expect(decoded).to.have.property('sub', 'foo');
72+
});
73+
});
74+
});
75+
});
76+
77+
describe('`jwt.decode`', function () {
78+
it('should still decode a token with a "crit" header, as it does not verify', function () {
79+
const token = signWithCrit(['http://example.invalid/UNDEFINED']);
80+
const decoded = jwt.decode(token, {complete: true});
81+
expect(decoded.header).to.have.deep.property('crit', ['http://example.invalid/UNDEFINED']);
82+
expect(decoded.payload).to.have.property('sub', 'foo');
83+
});
84+
});
85+
});

verify.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ module.exports = function (jwtString, secretOrPublicKey, options, callback) {
8383
}
8484

8585
const header = decodedToken.header;
86+
87+
//RFC 7515 Section 4.1.11: "crit" lists extension header parameters that must be
88+
//understood and processed, and the JWS is invalid if any of them are not. This
89+
//library implements no such extension, so any "crit" header is unsupported.
90+
if (typeof header.crit !== 'undefined') {
91+
return done(new JsonWebTokenError('unsupported "crit" header parameter'));
92+
}
93+
8694
let getSecret;
8795

8896
if(typeof secretOrPublicKey === 'function') {

0 commit comments

Comments
 (0)