JWT key confusion (alg)
TL;DR: Switch the token’s
algfromRS256toHS256so the verifier treats the RSA public key as an HMAC secret. Sign with that public key and the token validates.
What it is
Asymmetric algorithms (RS256, ES256) verify with a public key; symmetric algorithms (HS256) verify with a shared secret. Libraries that look up the verification key by kid and then dispatch on the header-supplied alg will hand the public key to the HMAC verifier when alg: HS256 is set. Because public keys are not secret, the attacker also knows them — and HMAC works with any byte string.
Preconditions / where it applies
- Verifier accepts the
algvalue from the token header instead of pinning it server-side - The RSA/EC public key is recoverable: published in JWKS, embedded in OIDC discovery, fetchable from
/.well-known/jwks.json, or extractable from token signatures withjwt-pubkey-recover - Library does not type-check that the configured key matches the requested algorithm
Technique
-
Grab the public key:
1 2
curl -s https://target.example/.well-known/jwks.json | jq .keys[0] # convert JWK to PEM if needed
-
Forge a token. Change the header
algtoHS256, set claims, sign with the public key as the HMAC secret:1 2 3
import jwt with open("pub.pem","rb") as f: pub = f.read() tok = jwt.encode({"sub":"admin","role":"admin","exp":9999999999}, pub, algorithm="HS256")
Important: use the exact byte representation the server uses (PEM with or without trailing newline, DER, JWK JSON). Trial both PEM variants — most failures here are formatting, not crypto.
-
Submit and check whether claims are accepted.
-
alg: nonevariant. Some legacy libraries accept{"alg":"none"}and skip verification entirely. Send a header-only token with no signature segment. -
Mixed-algorithm key store. If the server holds both an HMAC secret and an RSA pubkey indexed by
kid, switchingkidplusalgmay pick a different key class than intended.
Detection and defence
- Pin the algorithm server-side; never trust the header. APIs should call
verify(token, key, algorithms=["RS256"])and reject anything else - Use libraries that type-check key material against the algorithm (
PyJWT >=2,josewith explicit algs) - Store HMAC keys and RSA keys in separate keystores with non-overlapping
kidnamespaces - Reject
alg: noneat the gateway - Log alg mismatches between issuance and verification — a forged HS256 against an RS256 issuer is a high-signal alert
- Related: jwt-jku-jwk-injection for header-injected key material
References
- PortSwigger: algorithm confusion — labs and recovery technique
- CVE-2015-9235 jsonwebtoken — original public disclosure of the class
- jwt_tool — automation for this and other JWT attacks