On 11 September 2026 PyJWT 2.14.0 shipped alongside a dozen security advisories, and on 23 September 2.15.0 added two more. Fourteen CVEs in total, one rated Critical and five High in the GitHub Advisory Database, which published the reviewed entries between 29 September and 5 October.
PyJWT is the default JWT library for a large share of Python web apps, from Django and Flask projects to FastAPI services. The headline issues are all variations on one old bug class: getting a public key accepted as an HMAC secret. Whether you are exposed comes down to one argument in your jwt.decode() call.
What's affected
Fixed in 2.14.0 (all affect 2.13.0; several go back further):
| CVE | Rating | Issue |
|---|---|---|
| CVE-2026-102268 | Critical | Whitespace, CR-only or single-line PEM keys skip the asymmetric key guard |
| CVE-2026-102271 | High | Public keys in DER form accepted as HMAC secrets (2.4.0 onward) |
| CVE-2026-102272 | High | A UTF-8 byte order mark bypasses the JWK detection guard |
| CVE-2026-102273 | High | Public JWKs wrapped in a JWKS or array accepted as HMAC secrets |
| CVE-2026-102266 | High | PyJWK accepts an empty oct key |
| CVE-2026-102267 | High | PyJWKClient follows redirects when fetching a JWKS |
| CVE-2026-101917 | Moderate | Unknown kid values still force a fresh JWKS fetch each time |
| CVE-2026-102265 | Moderate | Deeply nested header raises RecursionError, not a PyJWT error |
| CVE-2026-102269 | Moderate | Non-canonical signature characters change the raw token string |
| CVE-2026-102270 | Moderate | ReDoS in is_pem_format |
| CVE-2026-102274 | Moderate | One malformed RSA key aborts parsing of a whole JWK Set |
| CVE-2026-103001 | Moderate | decode() mutates a reused options dict. Affects 2.11.0 to 2.13.0; the advisory points to the 2.14.0 changelog |
Fixed in 2.15.0 (affect 2.14.0 and earlier): CVE-2026-101918, a RecursionError in the pre-verification payload parse used by PyJWKClient.get_signing_key_from_jwt(), and CVE-2026-102275, Ed25519 and Ed448 JWKs whose x and d values don't match.
The four key confusion bugs share prerequisites. The Critical advisory spells them out: the algorithms allow-list "mixes an HMAC algorithm with an asymmetric one, e.g. algorithms=["ES256", "HS256"]", and the verification key is passed as raw key material. The attacker then signs an HS256 token using your public key, which is public by definition, as the HMAC secret.
How to check if you're exposed
Get the installed version:
pip show pyjwt | grep -i '^version'
Then look at how you call it. This one grep finds the configuration that gates the four confusion bugs:
grep -rnE "algorithms\s*=\s*\[[^]]*HS[0-9]+[^]]*(RS|ES|PS|EdDSA)|algorithms\s*=\s*\[[^]]*(RS|ES|PS|EdDSA)[^]]*HS[0-9]+" --include=*.py .
If it returns nothing, and you never build the algorithms list dynamically, the confusion family is not reachable in your code. A call like this is safe from all four:
jwt.decode(token, public_key, algorithms=["RS256"], audience="api")
Next, check the other gates:
grep -rn "PyJWKClient" --include=*.py . # CVE-2026-102267, 101917, 101918
grep -rn "verify_signature" --include=*.py . # CVE-2026-101918, 103001
grep -rn "options=" --include=*.py . | grep -i jwt # reused options dicts
Two of the denial of service bugs sit on ordinary paths. CVE-2026-102265 affects jwt.decode() on 2.13.0 for any unauthenticated token, because the header is parsed before the signature check. CVE-2026-101918 hits the first step of the documented JWKS flow. If your error handling only catches jwt.PyJWTError, a crafted token raises an exception you don't catch, and the request becomes a 500 or a dead worker.
What to do now
- Upgrade to PyJWT 2.15.0 or later. That covers all 14. In a lockfile-managed project, pin it explicitly, since PyJWT often arrives transitively through auth libraries.
- Never mix HMAC and asymmetric algorithms in one decode call. Choose the algorithm from trusted configuration per issuer, not from the token header.
- Load keys once and keep HMAC secrets and public keys in separate settings. The guards that failed here try to recognize public keys inside raw strings. Your configuration should never put the two in the same variable.
- Pin JWKS URLs to HTTPS endpoints you trust and avoid sending custom headers to them.
- Index token revocation by
jti, not by a hash of the raw token string. CVE-2026-102269 shows the same signature can be serialized more than one way. - Copy any shared
optionsdict before passing it todecode()if you can't upgrade immediately.
Why it matters
Algorithm confusion has been fixed in PyJWT before, under CVE-2022-29217. The fix relied on recognizing asymmetric key material by its text markers. These advisories found four encodings that slip past that check. That is the limit of guard rails: a library can only guess which strings are public keys, while the application knows exactly which algorithm each issuer uses.
So the configuration fix outlasts the version bump. An app that hard-codes algorithms=["RS256"] was never reachable by any of the four, on any version. An app with ["RS256", "HS256"] was reachable by all of them and may be again the next time someone finds a fifth encoding.
Try it yourself
Run the algorithms grep above across every Python repo you own. For each hit, write down which issuers really use HMAC and which use RSA or EC, then split the call into separate code paths. Add the grep to CI so a mixed list fails the build.
Click through it at your own pace, or open it full screen.
Quick check
An API calls jwt.decode(token, key, algorithms=["RS256"]). Can the key confusion advisories reach it?
No. All four require the allow-list to include an HMAC algorithm alongside an asymmetric one, so the token can claim HS256 and get verified with the public key as the secret.
Why can CVE-2026-102265 take down a service that catches jwt.PyJWTError?
Python's JSON decoder raises RecursionError on deeply nested input, which is not a PyJWT exception. On 2.13.0 it escaped jwt.decode() before any signature check, so an unsigned token was enough.
Which version fixes all 14 advisories?
2.15.0. Version 2.14.0 fixes 12; CVE-2026-101918 and CVE-2026-102275 needed 2.15.0.
Sources
- GHSA-ffc3-869f-jxw9: asymmetric PEM detection bypass
- GHSA-p4g4-x82p-q773: DER public keys as HMAC secrets
- GHSA-w2cx-738m-mc7w: public JWK containers as HMAC secrets
- GHSA-r6x4-923q-g947: BOM bypass
- GHSA-9j54-fg26-wv3r: empty HMAC keys via PyJWK
- GHSA-8wjv-2p76-3863: RecursionError in jwt.decode
- GHSA-42vr-xj54-vc7v: RecursionError in pre-verification payload parse
- GHSA-hxm8-2xgr-2p9m: non-canonical signature segments
- PyJWT 2.14.0 release
- PyJWT 2.15.0 release
- PyJWT on PyPI