Zion Boggan
repos/JWT Differential Fuzzer/findings/F001-nodejwt-crit-bypass.md
zionboggan.com ↗
98 lines · markdown
History for this file →
1
# node-jsonwebtoken: RFC 7515 §4.1.11 `crit` header parameter not enforced
2
 
3
| | |
4
|--|--|
5
| Affected | `jsonwebtoken` (npm), Auth0/`auth0/node-jsonwebtoken` |
6
| Tested version | 9.0.3 (current latest at time of testing) |
7
| Bug class | Spec violation of RFC 7515 §4.1.11 (Critical Header Parameter) |
8
| Sister advisories | CVE-2026-32597 (PyJWT, fixed in 2.12.0), CVE-2026-35042 (fast-jwt), CVE-2025-59420 (Authlib) |
9
| Status as of writing | No published advisory for this library |
10
 
11
## Summary
12
 
13
The `jsonwebtoken` package (`auth0/node-jsonwebtoken`) does not implement RFC 7515 §4.1.11 critical header processing. A signed JWS containing a `crit` header that lists extension parameter names the recipient does not understand MUST be rejected per the RFC ("the JWS is invalid"). The library accepts such tokens unconditionally as long as the signature is valid for the declared `alg`.
14
 
15
The same bug class has been disclosed and patched in three sister libraries (PyJWT, fast-jwt, Authlib). This advisory covers the same defect in `jsonwebtoken`, where it remains unpatched.
16
 
17
## RFC reference
18
 
19
> "If any of the listed extension Header Parameters are not understood and supported by the recipient, then the JWS is invalid."
20
> - RFC 7515 §4.1.11
21
 
22
## Source-code confirmation
23
 
24
`verify.js` in the current `master` branch does not reference `crit`, RFC 7515 §4.1.11, RFC 7797, `b64`, or any extension-header-parameter handling. The verification path validates only `alg`, `nbf`, `exp`, `aud`, `iss`, `sub`, `jti`, `nonce`, `iat`, and `maxAge`. There is no code path that inspects the `crit` array.
25
 
26
## Reproduction
27
 
28
Differential test against the four major JWT libraries:
29
 
30
```
31
$ python3 orchestrator/differ.py --corpus corpus/seed.json --only crit-crit-eca
32
[schism] 4/5 targets up: ['nodejwt', 'pyjwt', 'pyjose', 'panva']
33
[BYPASS] crit-crit-eca                        sev=bypass-risk  accept=['nodejwt', 'pyjose']  reject=['pyjwt', 'panva']
34
```
35
 
36
Per-library verdict on the test case:
37
 
38
```
39
nodejwt 9.0.3  valid=True
40
pyjwt   2.12.0 valid=False  InvalidJWTError: Token has unsupported critical header
41
pyjose  3.3.0  valid=True
42
panva   5.10.0 valid=False  ERR_JOSE_NOT_SUPPORTED: Extension Header Parameter "foobar" is not recognized
43
```
44
 
45
Test token construction (script-equivalent):
46
 
47
```js
48
const jwt = require("jsonwebtoken");
49
const secret = "schism-secret";
50
 
51
// header: {"alg":"HS256","typ":"JWT","crit":["foobar"],"foobar":true}
52
// crit declares "foobar" is critical; "foobar" is not registered or
53
// understood by jsonwebtoken or any standard extension.
54
const token = jwt.sign(
55
  { sub: "alice", iat: 1700000000, exp: 9999999999 },
56
  secret,
57
  { algorithm: "HS256", header: { crit: ["foobar"], foobar: true } }
58
);
59
 
60
const result = jwt.verify(token, secret, { algorithms: ["HS256"] });
61
console.log(result);
62
// → { sub: "alice", iat: 1700000000, exp: 9999999999 }
63
//   token is ACCEPTED despite unknown critical extension
64
```
65
 
66
Expected behavior per RFC 7515 §4.1.11: `jwt.verify` should throw because the recipient does not support the `foobar` extension.
67
 
68
## Exploitability
69
 
70
Per-extension scenarios where unenforced `crit` enables a real bypass:
71
 
72
1. **RFC 7797 unencoded payload (`b64=false`).** A producer that signs detached payloads with `crit:["b64"], b64:false` expects strict verifiers to demand the detached payload separately and reject any in-band attempt. `jsonwebtoken` ignores the directive, treats the token as a standard compact JWS, and validates whatever happens to sit in the second segment. In a heterogeneous fleet (e.g. `jsonwebtoken` on a backend, panva at an API gateway), an attacker can exploit the split-brain interpretation to substitute payloads.
73
 
74
2. **RFC 9449 DPoP-bound tokens (`cnf` declared critical).** Authorization servers that bind tokens to a proof-of-possession key by listing `cnf` in `crit` rely on the verifier rejecting tokens it cannot DPoP-validate. With `jsonwebtoken`, the cnf binding is silently dropped and the token is accepted as a bearer token, defeating the proof-of-possession guarantee.
75
 
76
3. **Custom application extensions.** Any application that issues tokens with critical custom claims (e.g. `crit:["x-tenant-pin"]`) cannot rely on `jsonwebtoken` to enforce them. Tokens that should be rejected for extension non-support are accepted.
77
 
78
## Comparison with sister advisories
79
 
80
| Lib | Advisory | Fixed |
81
|--|--|--|
82
| PyJWT | CVE-2026-32597 / GHSA-752w-5fwx-jx9f | 2.12.0 |
83
| fast-jwt | CVE-2026-35042 | (per advisory) |
84
| Authlib | CVE-2025-59420 / GHSA-9ggr-2464-2j32 | (per advisory) - CVSS 7.5 |
85
| **jsonwebtoken (this report)** | **none** | **unpatched** |
86
 
87
## Suggested remediation
88
 
89
`verify.js` should, after JSON-parsing the header and before signature verification:
90
 
91
1. If `header.crit` is present, validate it is a non-empty array of strings (per RFC 7515 §4.1.11 "MUST NOT be empty" and ABNF).
92
2. Forbid reserved Header Parameter names and entries not present in the JOSE Header.
93
3. Reject the token if any entry is not in a caller-supplied set of supported extensions.
94
4. Provide a public API (e.g. an option `crit: string[]` on `verify`) for callers to declare which critical extensions their application understands, mirroring `panva/jose`'s contract.
95
 
96
## Disclosure
97
 
98
Filed via GitHub Security Advisory at `auth0/node-jsonwebtoken`. CVE requested via MITRE / GitHub.