# PyJWT: Asymmetric-PEM detection bypass: whitespace/line-ending-mutated public keys skip the HS/asymmetric confusion guard

PyJWT's is_pem_format regex misses loader-accepted PEM mutations, so a public key becomes the HS256 secret and tokens forge as superadmin under mixed algorithm allow-lists.

- Payout: Research
- Severity: critical
- Program: PyJWT
- Found by: e1024x
- Disclosed: 2026-09-29
- CVE: CVE-2026-102268
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-ffc3-869f-jxw9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-ffc3-869f-jxw9
- Tags: area:oss domain:oss impact:integrity impact:privesc kind:research platform:ghsa sev:critical tech:python technique:incomplete-fix technique:parser-differential vuln:auth-bypass vuln:crypto

**Target:** jwt.decode HMAC key preparation (jwt/algorithms.py HMACAlgorithm.prepare_key)
**Root cause:** The asymmetric-key rejection in HMACAlgorithm.prepare_key is gated on is_pem_format(), whose regex requires LF terminators and marker-adjacent [- ] only, while cryptography's load_pem_public_key accepts tabs, CR-only terminators and single-line PEM. A loader-accepted but regex-missed PEM falls through and is used as the HMAC secret.
**Steps:**
1. Generate EC P-256 or RSA-2048 keypair; serialize public key to PEM.
2. Mutate PEM into loader-accepted, regex-missed form: insert a tab before `-----END`, convert terminators to bare `\r`, or join all lines into one.
3. Confirm `jwt.utils.is_pem_format(mutated) is False` and `cryptography.hazmat.primitives.serialization.load_pem_public_key(mutated)` succeeds.
4. `jwt.encode({"sub":"superadmin"}, mutated, algorithm="HS256")`, then `jwt.decode(token, mutated, algorithms=["ES256","HS256"])` — verification succeeds.
**How found:** Noticed the CVE-2022-29217 fix relies on is_pem_format() as the sole family-mismatch guard, then compared the regex's accepted byte-forms against cryptography's tolerant PEM loader, hypothesizing a parser differential on whitespace/line endings.
**Exploitation:** Attacker needs only the public verification key. With a deployment that passes raw PEM (non-PyJWK path) in a mutated byte-form and an allow-list mixing HMAC with asymmetric (e.g. ["ES256","HS256"]), the attacker signs an HS256 token using the public-key text as the HMAC secret; decode skips the mismatch guard and accepts arbitrary claims such as sub=superadmin.
**Insight:** When a security guard is a hand-rolled format recognizer sitting in front of a tolerant third-party parser, enumerate byte-forms the parser accepts but the recognizer rejects (tabs, CR-only, folded lines) — the guard is only as strong as the narrower of the two.

## Proof of concept (copied exactly from the original report)

**Forged HS256 token verified with mutated public key**

```
jwt.encode({"sub":"superadmin"}, mutated, algorithm="HS256")`, then `jwt.decode(token, mutated, algorithms=["ES256","HS256"])
```

**Guard check on is_pem_format in prepare_key**

```
if is_pem_format(key_bytes) or is_ssh_key(key_bytes):
        raise InvalidKeyError(
            "The specified key is an asymmetric key or x509 certificate and"
            " should not be used as an HMAC secret."
        )
```

