JSON Web Tokens are everywhere now, and in our assessment work they remain one of the most reliable places to find an authentication bypass. The token format is not the problem. The problem is that verifying a JWT is more subtle than it looks, and a handful of implementation shortcuts turn a stateless session token into a forgery kit. This is a consolidated field guide to the four issues we find most often, why each one works, and what actually fixes it.
A quick refresher first. A signed JWT is three base64url segments joined by dots: a header, a payload of claims, and a signature. The header names the algorithm in its alg field, the payload carries the identity and its claims, and the signature covers the first two. Every attack below is ultimately about the same thing: getting a server to accept a signature it should have rejected. The definitions are in RFC 7519.
1. The alg:none downgrade
The specification defines an none algorithm for unsigned tokens. It exists for cases where integrity is guaranteed by another layer, and it is a loaded gun pointed at any verifier that does not explicitly refuse it.
- The attack. Take a valid token, change the header
algtonone, edit the payload to claim whatever identity you want, and drop the signature entirely (the third segment becomes empty). A verifier that honoursnonetreats the token as valid with no signature to check. - Impact. Full authentication bypass. Set
suborroleto an administrator and you are one. - Fix. Reject
noneoutright, and verify against an explicit allow-list of expected algorithms rather than whatever the token asks for.
2. RS256 to HS256 algorithm confusion
This is the subtle one, and the one that survives in mature codebases. Asymmetric signing (RS256) uses a private key to sign and a public key to verify. Symmetric signing (HS256) uses one shared secret for both. The public key is, by design, public.
- The attack. The server expects RS256 and holds the RSA public key to verify with. The attacker changes the header to
HS256, then computes an HMAC over the token using the RSA public key bytes as the HMAC secret. If the verifier picks its algorithm from the token header, it now runs HS256 and uses the public key as the shared secret, exactly the value the attacker just used. The signature matches. - Why it works. A single "verify(token, key)" call that trusts the header's algorithm is the root cause. The key that was safe to publish becomes a signing secret the moment the algorithm is switched under it.
- Fix. Pin the algorithm at the verification call. The server, not the token, decides that this token must be RS256, and a token claiming anything else is rejected before a key is ever loaded.
3. Weak HMAC secrets
When a system does use HS256 correctly, the entire security of every session rests on one shared secret. We have recovered that secret offline more than once.
- The attack. An HS256 token is a hash the attacker can verify against a candidate secret entirely offline, with no requests to the target. Feed the token to a cracker (hashcat mode 16500) with a wordlist, and a weak or default secret falls in minutes. With the secret, the attacker mints valid tokens at will.
- Fix. Use a long, high-entropy random secret, treat it as a credential, and rotate it. A memorable string is not a secret.
4. Header injection: jwk, jku, and kid
The header can carry key material or a pointer to it, and a verifier that trusts those fields is trusting the attacker.
- jwk. An attacker embeds their own public key in the token header and signs with the matching private key. A verifier that validates against the embedded key accepts it. Never verify against a key supplied by the token.
- jku. The header points to a URL hosting the key set. If the server fetches and trusts an attacker-controlled URL, the attacker supplies their own keys. Restrict the URL to an allow-list, or do not honour it at all.
- kid. The key id selects which key to use, and because it is often used to look a key up in a file or database, we have seen it carry path traversal and SQL injection. Treat it as untrusted input.
What we recommend
The through-line is that the token is attacker-controlled data and the verifier must never let it choose its own terms of verification. In order of what pays off fastest:
- Pin the expected algorithm at verification and reject everything else, which closes both
noneand RS256-to-HS256 in one move. - Ignore
jwkandjkufrom incoming tokens; resolve keys from your own trusted store keyed by a validatedkid. - Use strong random secrets for HMAC and real key management for asymmetric keys.
- Validate the standard claims you are relying on, especially
exp,aud, andiss. A signature that verifies is not the same as a token you should honour.
For a hands-on reference to test your own tokens against, the PortSwigger Web Security Academy JWT material is the clearest walkthrough of each of these, and the jwt.io introduction is the plainest reference for the token format and the claims worth validating. If you run JWT-based authentication and have never checked which of these your library defends against by default, that is an afternoon well spent.