Safeguard
Application Security•

PyJWT 2.14 and 2.15: 14 Advisories, and the Config That Makes Them Real

PyJWT fixed 14 issues in September, including four new ways around its HS/RS key confusion guard. Most need a mixed algorithms list. Check yours in one grep.

Safeguard Security Team
6 min read

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):

CVERatingIssue
CVE-2026-102268CriticalWhitespace, CR-only or single-line PEM keys skip the asymmetric key guard
CVE-2026-102271HighPublic keys in DER form accepted as HMAC secrets (2.4.0 onward)
CVE-2026-102272HighA UTF-8 byte order mark bypasses the JWK detection guard
CVE-2026-102273HighPublic JWKs wrapped in a JWKS or array accepted as HMAC secrets
CVE-2026-102266HighPyJWK accepts an empty oct key
CVE-2026-102267HighPyJWKClient follows redirects when fetching a JWKS
CVE-2026-101917ModerateUnknown kid values still force a fresh JWKS fetch each time
CVE-2026-102265ModerateDeeply nested header raises RecursionError, not a PyJWT error
CVE-2026-102269ModerateNon-canonical signature characters change the raw token string
CVE-2026-102270ModerateReDoS in is_pem_format
CVE-2026-102274ModerateOne malformed RSA key aborts parsing of a whole JWK Set
CVE-2026-103001Moderatedecode() 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

  1. 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.
  2. Never mix HMAC and asymmetric algorithms in one decode call. Choose the algorithm from trusted configuration per issuer, not from the token header.
  3. 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.
  4. Pin JWKS URLs to HTTPS endpoints you trust and avoid sending custom headers to them.
  5. 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.
  6. Copy any shared options dict before passing it to decode() 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

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.