Algorithm Confusion Attacks
One of the most dangerous JWT vulnerabilities involves the alg field in the token header. Because the header is not encrypted (only signed), an attacker can modify it before sending the token.
The alg: none attack: An attacker strips the signature and sets alg: none, hoping the verifier accepts unsigned tokens. Defense: always explicitly specify which algorithms you accept. Never allow none.
The RS256 → HS256 confusion attack: If a server is configured to accept both RS256 and HS256, an attacker can:
1. Take a valid RS256 token.
2. Change the header to alg: HS256.
3. Re-sign the token using the server's public key as the HMAC secret (the public key is, by definition, public).
4. The server, now treating it as HS256, verifies the HMAC using its own public key — and accepts the forged token.
Defense: always pin the expected algorithm in your verification code. Do not read alg from the token header to decide how to verify.
# WRONG — trusts the token's alg claim
decoded = jwt.decode(token, public_key)
# CORRECT — pins the algorithm server-side
decoded = jwt.decode(token, public_key, algorithms=["RS256"])
kid Injection
If your code constructs a database query or file path using the kid value from the token header without sanitization, an attacker can inject malicious values. For example, if your code does:
key = load_key_from_db(f"SELECT key FROM keys WHERE kid = '{kid}'")
An attacker sets kid to ' OR '1'='1 and retrieves all keys. Defense: treat kid as untrusted input. Validate it against an allowlist of known key IDs before using it in any lookup.
JWKS Endpoint Spoofing
Some poorly implemented systems allow the token itself to specify the JWKS URL (via a custom jku header claim). An attacker can point this to their own server hosting a JWKS with their own public key, then sign tokens with the corresponding private key.
Defense: never trust a JWKS URL from the token. Hardcode or configure the JWKS URI in your application. Only fetch from the known, trusted auth server URL.