JWT Algorithm Confusion Attack
JWT algorithm confusion tricks RS256 verifiers into accepting HS256-signed tokens using the RSA public key as the HMAC secret. Learn how the attack works, the step-by-step exploit, and the one-line fix.
Algorithm confusion (RS256 → HS256) exploits any verifier that reads `alg` from the token header instead of enforcing a fixed algorithm. It works specifically against RS256 systems because RS256's public key is available to attackers by definition. The attacker signs a forged token with HMAC-SHA256 using the RSA public key as the secret, and the vulnerable server accepts it.
How the attack works
The server uses RS256: it signs with a private RSA key and verifies with the public RSA key. The attacker obtains the public key (from `/.well-known/jwks.json`, a certificate, source code, or an API response), crafts a token with `"alg": "HS256"` in the header containing whatever payload they want, signs that token with HMAC-SHA256 using the RSA public key (in its PEM string form) as the HMAC secret, and submits it. The vulnerable server reads `alg: HS256` from the header, retrieves its RSA public key, uses it as the HMAC secret for verification, and the signatures match: the server accepts a completely forged token.
The fix
Hardcode the expected algorithm. If you use RS256, only accept RS256. Never fall back to HS256. An algorithms array like `['RS256', 'HS256']` is almost always wrong: do not include both unless you have a specific, documented reason and have thought through the implications for key handling. The fix is a single config change in every modern library.
Code examples
The attack (Python: RSA public key as HMAC secret)
import hmac, hashlib, base64, json
# Public key obtained from /.well-known/jwks.json or similar
public_key = b"-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----"
def b64url(data):
if isinstance(data, dict):
data = json.dumps(data, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(data).rstrip(b"=")
header = b64url({"alg": "HS256", "typ": "JWT"})
payload = b64url({"sub": "1", "role": "admin", "exp": 9999999999})
signing_input = header + b"." + payload
# The RSA public key used as the HMAC secret
sig = hmac.new(public_key, signing_input, hashlib.sha256).digest()
signature = base64.urlsafe_b64encode(sig).rstrip(b"=")
forged = (signing_input + b"." + signature).decode() The fix (Node.js: jsonwebtoken)
// RS256 system: only accept RS256
const decoded = jwt.verify(token, publicKey, { algorithms: ["RS256"] });
// HS256 system: only accept HS256
const decoded = jwt.verify(token, secret, { algorithms: ["HS256"] });
// ['RS256', 'HS256'] is almost always wrong: do not include both. Frequently asked questions
-
What is JWT algorithm confusion?
Algorithm confusion is an attack where a verifier trusts the alg field in the token header instead of enforcing a fixed algorithm server-side. Against an RS256 system, the attacker obtains the RSA public key (which is public by definition), crafts a token with alg: HS256, and signs it with HMAC-SHA256 using the RSA public key as the HMAC secret. The vulnerable verifier reads alg: HS256, uses the RSA public key as the HMAC secret, and accepts the forged token. The fix is to hardcode the expected algorithm and never accept both RS256 and HS256 in the same verifier.
-
How do I prevent JWT algorithm confusion?
Hardcode your expected algorithm in every verification call. If you use RS256, configure your library to only accept RS256 (jwt.verify(token, key, { algorithms: ['RS256'] })) and never fall back to HS256. Never put both RS256 and HS256 in the algorithms array unless you have a documented architectural reason. The fix is a single config change in jsonwebtoken, PyJWT, jjwt, jose, and every other modern JWT library.
-
Why does algorithm confusion only work against RS256 systems?
Because RS256 uses a public key for verification, and public keys are available to attackers by definition: at /.well-known/jwks.json, in certificates, or in source code. The attacker needs the verification key to forge an HS256 token that the verifier will accept. Against an HS256 system, the verification key is the shared secret, which the attacker does not have: so they cannot forge a token that passes verification. The attack specifically exploits the asymmetry of RS256 combined with a verifier that trusts the token header's alg.