# There&#8217;s a ten-year-old key confusion bug in PyJWT, fixed in 2.14.0

**URL:** https://mazehq.com/blog/theres-a-ten-year-old-key-confusion-bug-in-pyjwt-fixed-in-2-14-0
**Date:** 2026-09-16

There’s a new key confusion bug in PyJWT that lets an attacker forge login tokens using nothing but your public key. With over 590 million downloads a month, PyJWT is the most downloaded JWT library in Python, and a lot of authentication sits on top of it.

This vulnerability has been fixed in 2.14.0 and is tracked as [GHSA-p4g4-x82p-q773](https://github.com/jpadilla/pyjwt/security/advisories/GHSA-p4g4-x82p-q773).

This vulnerability stayed hidden for over a decade because no tool was capable of finding it. Bugs like this won’t stay hidden anymore. It’s getting easier and cheaper to find novel vulnerabilities like this one, and defenders need to find them and patch them before attackers do.

This time Maze Code got there first during a routine scan of PyJWT. Our agents found this flaw by reading PyJWT’s key handling code and reasoning about what its existing key confusion protection actually protected against, like your best security engineer would.

In this blog, we’ll walk through who’s affected, the fix, the bug itself, and how Maze Code found it.

*Credit to the PyJWT maintainers, who were quick and great to work with.*

## Are you affected?

You’re affected if all three of these prerequisites are met:

- You call `jwt.decode` with an `algorithms` list that mixes an HS* algorithm with an asymmetric one, like `["RS256", "HS256"]`.
- The key you pass in is raw DER bytes: a DER SubjectPublicKeyInfo, DER PKCS#1 public key, or DER X.509 certificate.
- You’re on PyJWT 2.13.0 or earlier.

The advisory lists 2.13.0 as affected, but in practice the bug goes back further. PyJWT’s check that prevents asymmetric keys from being used as HMAC secrets has worked the same way since it was added in 2015. Our proof of concept ran identically on every release we tested from 2.3.0 through 2.13.0. Treat everything up to and including 2.13.0 as affected.

You’re not affected if any one of these conditions doesn’t apply. PEM keys are caught by PyJWT’s existing check. PyJWT 2.14.0 adds a check for DER keys. An asymmetric-only `algorithms` list rejects the forged HS256 token before PyJWT ever looks at the key. You’re also not affected if you use `PyJWK` or `PyJWKClient`, since verification gets a parsed key object rather than raw bytes.

Even if you’re not affected today, avoid mixing symmetric and asymmetric algorithms in the same `algorithms` list. The underlying problem is how your application passes keys to PyJWT, and a future bypass could involve a different key format.

## The fix

[Upgrade to PyJWT 2.14.0](https://github.com/jpadilla/pyjwt/releases/tag/2.14.0), released September 11, 2026. The new version checks DER-encoded keys in `HMACAlgorithm.prepare_key` before using them as HMAC secrets and rejects them if they parse as an asymmetric key.

If you can’t upgrade today, split your `algorithms` list. One key, one algorithm:

`algorithms=["RS256"]`

This closes the attack path on every PyJWT version because the forged HS256 token is rejected before PyJWT ever looks at the key.

If you upgrade to 2.14.0 but continue mixing symmetric and asymmetric algorithms in the same list, you could still be exposed to a future key confusion bypass. The safer fix is to use a single algorithm for each key.

## How it works

This is a key confusion vulnerability, andPyJWT supports two fundamentally different ways of signing JWTs:

- Asymmetric algorithms like RS256 use a private key to sign tokens and a public key to verify them.
- Symmetric algorithms like HS256 use the same secret to sign and verify tokens.

The attack works when an application tells PyJWT to accept both types:

`algorithms=["RS256", "HS256"]`

An attacker who has the application’s public key can then try to use that public key as an HMAC secret and forge an HS256 token.

### Why the existing check missed DER keys

PyJWT already had a check designed to prevent asymmetric public keys from being used as HMAC secrets. It was added in response to [CVE-2022-29217](https://nvd.nist.gov/vuln/detail/cve-2022-29217), and it’s what most teams assume is protecting them.

For example, a PEM-encoded public key is caught by the existing check. The same public key encoded as DER bytes was not.

That meant an attacker could take a legitimate public key, convert it to DER, and use those bytes as the HS256 signing secret.

### The vulnerable path

An application might verify tokens like this:

```
jwt.decode(
    token,
    public_key,
    algorithms=["RS256", "HS256"]
)

```

The attacker doesn’t need the private key. They:

1. Obtain the application’s public key.
2. Convert it to the DER format PyJWT’s check doesn’t reject.
3. Use those bytes as the HMAC secret.
4. Create an HS256 token with whatever claims they want.
5. Send the forged token to the application.

Because HS256 is in the allowed `algorithms` list, PyJWT accepts the token and uses the supplied public key bytes as the HMAC secret.

### Why 2.14.0 fixes it

PyJWT 2.14.0 adds a DER parsing check before the key can be used as an HMAC secret. If the bytes parse as an asymmetric key, PyJWT rejects them.

The important point is that the vulnerability isn’t just about DER. The underlying dangerous pattern is allowing symmetric and asymmetric algorithms to share the same verification path.

That is why the safest configuration is still simple: one key, one algorithm.

### Proof of concept

Our proof of concept demonstrates the attack using a DER-encoded public key. It worked identically across every PyJWT release we tested from 2.3.0 through 2.13.0.

```
pub = rsa.generate_private_key(public_exponent=65537, key_size=2048).public_key()
pem = pub.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)

# PEM form is rejected, as expected since CVE-2022-29217.
jwt.decode(forge(pem), pem, algorithms=["RS256", "HS256"])
# InvalidKeyError: The specified key is an asymmetric key or x509 certificate...

# Same key, DER form. The forged token verifies.
jwt.decode(forge(der), der, algorithms=["RS256", "HS256"])
# {'user': 'admin', 'role': 'admin'}

```

## How Maze Code found it

We routinely run our agents against open source projects to find novel vulnerabilities before attackers do, keeping open source secure. Our agents were working their way through PyJWT’s key handling and stopped at the `if` statement that rejects asymmetric keys. On paper, it does exactly what it says. In practice, it only looks at the key as text, searching for a `BEGIN` header or an `ssh-` prefix. It never parses the key to find out what it is.

The history of patches on that `if` statement is what made the agents look twice. It started in 2015 as a list of four byte strings to reject: two PEM headers, one for certificates, and `ssh-rsa`. In 2022, someone reported an SSH key format the list didn’t cover (CVE-2022-29217), so 2.4.0 swapped in a PEM regex and a longer SSH list. In 2.13.0, someone reported a JWK passed as raw JSON (GHSA-xgmm-8j9v-c9wx), so a JSON check went in.

Three reports, three correct fixes, and every one of them added another key format to the reject list. The rejected formats list kept growing, but a list can only cover the formats someone has already reported.

PyJWT never looks for DER. There isn’t a single line of DER handling in the whole package. So we passed in the same key with the PEM wrapper stripped off, and it went straight through.

This is the kind of bug that doesn’t have an obvious pattern for a scanner to match. Nothing here is a bad function call or unsanitized input. The line a scanner would flag is a `raise,` which is the existing check working exactly as designed.

Maze Code’s full code review doesn’t rely on rules. It reads the codebase the way a security researcher would, working out what each piece of code is meant to guarantee and testing whether it actually does. Here, that meant reading the if statement, asking what an asymmetric key can look like, noticing DER wasn’t on the list, and building the proof of concept to confirm it.

That’s how a vulnerability that had stayed hidden for more than a decade was found.

## About Maze Code

Maze was built to reason, not run on rules, delivering security that cuts through the noise and surfaces what’s real. Our agents investigate every vulnerability across your dependencies, code, and cloud to find and fix what matters.

Maze Code agents have found exploitable CVEs just like this one in multiple open source projects scanned by other AI-SAST tools. Learn more about [Maze Code.](https://mazehq.com/platform/code)