Zion Boggan
repos/jwt-differential-fuzzer/findings/F002-pyjose-crit-bypass.md
zionboggan.com ↗
98 lines · markdown
History for this file →
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.