Step 1 of 5 · Reading · ~3 min
Read
Production Concerns
Known JWT Attacks
Every JWT break has one shape. RFC 7519 §11.1: a token can be relied on only when its contents are cryptographically secured and the key is verifiably controlled by the issuer named in it. Break either half and the token says whatever the attacker wants.
1. Algorithm confusion: turning the public key into the secret
This one gets real systems, because it attacks a verifier doing everything else right. Picture a service verifying RS256 tokens from an identity provider, holding one thing: the provider's public key — published, because that is how verification works. Watch a naive verifier:
Change alg from RS256 to HS256: dispatch lands on the HMAC branch, where
key — the RSA public key — becomes a shared secret the attacker already
downloaded. So they sign for themselves:
That token verifies. No key stolen, no cryptography broken — the verifier was persuaded to use a public value as a private one.
The defense is not "check the alg" but to stop letting the token into the decision:
Bind the algorithm to the key, not the header. If your code can hand a public key to an HMAC function, the bug is the shape of your code.
2. The none algorithm
Rewrite the header to {"alg":"none"} and delete the signature. RFC 7519 §6 really
does define this: an Unsecured JWT is alg of "none" with the empty string as
its signature, so a genuine one ends in a bare dot. Same root cause, same fix; it
has a lesson of its own.
3. Guessable HMAC secrets
secret, changeme, your-256-bit-secret. No server access needed: capture a token
and grind a wordlist offline, with no rate limit. Cracking tools ship JWT modes for
this (hashcat's 16500).
4. Tokens in URLs
GET /report?token=eyJ... writes a live credential into browser history, access
logs, proxy logs, and the Referer header.
5. Long lifetimes
A stateless token cannot be revoked, only expire — so exp is your whole revocation
story and the lifetime you pick is the attacker's window. Minutes, not months.
6. Sensitive data in the payload
Base64 is not encryption. Card numbers, national IDs and password hashes have all shipped in production JWTs, readable by anyone holding one.
Summary
| Attack | Root cause | Defense |
|---|---|---|
| Algorithm confusion | token picks the algorithm | pin it, bound to key type |
alg: none | token picks no algorithm | reject none, any casing |
| Weak secret | key chosen by a human | 32+ CSPRNG bytes |
| Token in URL | credential somewhere loggable | Authorization |
Long exp | expiry is the only revocation | short exp + refresh |
| Data in payload | encoding taken for encryption | store an id |
Your exercise
You will write a linter: one token per line, issues comma-separated or OK. Two
things the grader is strict about:
- Order. A token both unsigned and unexpiring prints exactly
alg_none,no_expiration. Issues emit asalg_none,no_expiration,long_lived,pii_in_payload— not the order you detect them. long_livedneeds both endpoints.exp - iat > 86400, so a payload withexpand noiathas no computable lifetime and must not be flagged: the visible{"sub":"x","password":"hunter2","exp":3600}ispii_in_payloadonly.
The third segment is a placeholder — the linter never inspects it.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…