| 1 | # python-jose: RFC 7515 §4.1.11 `crit` header parameter not enforced |
| 2 | |
| 3 | | | | |
| 4 | |--|--| |
| 5 | | Affected | `python-jose` (PyPI), `mpdavis/python-jose` | |
| 6 | | Tested version | 3.3.0 (current latest, last released 2022) | |
| 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 | `python-jose` does not implement RFC 7515 §4.1.11 critical header processing. A signed JWS that declares unknown extension parameters as critical via the `crit` header MUST be rejected per the RFC. `python-jose` accepts such tokens unconditionally if the signature is otherwise valid. |
| 14 | |
| 15 | The same defect was patched in PyJWT (2.12.0) under CVE-2026-32597 and similar in Authlib (CVE-2025-59420, CVSS 7.5) and fast-jwt (CVE-2026-35042). `python-jose` remains unpatched. |
| 16 | |
| 17 | ## Maintenance status caveat |
| 18 | |
| 19 | `python-jose` has not had a release since 3.3.0 (2022) and the GitHub advisory page shows zero published security advisories. The library is widely deployed via transitive dependencies (FastAPI dependency chains, Authlib alternatives, OAuth2 clients) and is not formally deprecated. A CVE here serves both as user notification and as input for downstream forks. |
| 20 | |
| 21 | ## RFC reference |
| 22 | |
| 23 | > "If any of the listed extension Header Parameters are not understood and supported by the recipient, then the JWS is invalid." |
| 24 | > - RFC 7515 §4.1.11 |
| 25 | |
| 26 | ## Source-code confirmation |
| 27 | |
| 28 | `jose/jws.py` in the current `master` branch does not reference `crit`, RFC 7515 §4.1.11, RFC 7797, `b64`, or any extension-header-parameter handling. The `_load()` function decodes the header JSON without inspecting `crit`. The `_encode_header()` function passes through arbitrary additional headers but performs no validation. |
| 29 | |
| 30 | ## Reproduction |
| 31 | |
| 32 | ``` |
| 33 | $ python3 orchestrator/differ.py --corpus corpus/seed.json --only crit-crit-eca |
| 34 | [schism] 4/5 targets up: ['nodejwt', 'pyjwt', 'pyjose', 'panva'] |
| 35 | [BYPASS] crit-crit-eca sev=bypass-risk accept=['nodejwt', 'pyjose'] reject=['pyjwt', 'panva'] |
| 36 | ``` |
| 37 | |
| 38 | Per-library verdict: |
| 39 | |
| 40 | ``` |
| 41 | nodejwt 9.0.2 valid=True |
| 42 | pyjwt 2.12.0 valid=False InvalidJWTError: Token has unsupported critical header |
| 43 | pyjose 3.3.0 valid=True |
| 44 | panva 5.10.0 valid=False ERR_JOSE_NOT_SUPPORTED: Extension Header Parameter "foobar" is not recognized |
| 45 | ``` |
| 46 | |
| 47 | Standalone reproducer: |
| 48 | |
| 49 | ```python |
| 50 | from jose import jwt |
| 51 | import base64, hmac, hashlib, json |
| 52 | |
| 53 | secret = "schism-secret" |
| 54 | header = {"alg": "HS256", "typ": "JWT", "crit": ["foobar"], "foobar": True} |
| 55 | claims = {"sub": "alice", "iat": 1700000000, "exp": 9999999999} |
| 56 | |
| 57 | def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode() |
| 58 | h = b64u(json.dumps(header, separators=(",", ":")).encode()) |
| 59 | p = b64u(json.dumps(claims, separators=(",", ":")).encode()) |
| 60 | sig = hmac.new(secret.encode(), f"{h}.{p}".encode(), hashlib.sha256).digest() |
| 61 | token = f"{h}.{p}.{b64u(sig)}" |
| 62 | |
| 63 | result = jwt.decode(token, secret, algorithms=["HS256"]) |
| 64 | print(result) |
| 65 | # → {'sub': 'alice', 'iat': 1700000000, 'exp': 9999999999} |
| 66 | # token ACCEPTED despite unknown critical "foobar" extension |
| 67 | ``` |
| 68 | |
| 69 | Expected behavior per RFC 7515 §4.1.11: `jwt.decode` should raise because `foobar` is not understood. |
| 70 | |
| 71 | ## Exploitability |
| 72 | |
| 73 | Same as the sister advisories: any application that relies on `crit` to enforce a security-relevant extension cannot rely on `python-jose` to honor the directive. Specific scenarios - RFC 7797 detached payload, RFC 9449 DPoP `cnf` binding, custom application extensions - are silently bypassed. |
| 74 | |
| 75 | In heterogeneous deployments where one service uses `python-jose` and another uses a strict verifier (panva, PyJWT ≥ 2.12.0), the same token bytes parse with different security guarantees at different services. This split-brain validation is the same exploitation pattern documented in CVE-2025-59420 (Authlib). |
| 76 | |
| 77 | ## Comparison with sister advisories |
| 78 | |
| 79 | | Lib | Advisory | Fixed | |
| 80 | |--|--|--| |
| 81 | | PyJWT | CVE-2026-32597 / GHSA-752w-5fwx-jx9f | 2.12.0 | |
| 82 | | fast-jwt | CVE-2026-35042 | (per advisory) | |
| 83 | | Authlib | CVE-2025-59420 / GHSA-9ggr-2464-2j32 | CVSS 7.5 | |
| 84 | | **python-jose (this report)** | **none** | **unpatched** | |
| 85 | |
| 86 | ## Suggested remediation |
| 87 | |
| 88 | In `jose/jws.py` `_verify_signature` and `_load`: |
| 89 | |
| 90 | 1. After parsing the JOSE header, check for a `crit` member. |
| 91 | 2. Validate the value is a non-empty list of strings, each corresponding to a Header Parameter actually present in the header (per RFC 7515 §4.1.11 ABNF and prose). |
| 92 | 3. Reject reserved RFC names from appearing in `crit`. |
| 93 | 4. Reject the token if any entry is not in an explicit caller-supplied allowlist of supported extensions. |
| 94 | 5. Expose this allowlist via the public `jwt.decode` and `jws.verify` APIs. |
| 95 | |
| 96 | ## Disclosure |
| 97 | |
| 98 | Filed via GitHub Security Advisory at `mpdavis/python-jose`. CVE requested via MITRE. |