| 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. |