JWT Attacks: The Complete Practical Guide
Preventing JWT Attacks
How to stop JWT attacks: pin the algorithm, reject none, use strong keys, ignore attacker-controlled headers, and validate claims, with per-library code.
Published
Every attack in this cluster comes back to one root cause: the server trusted a token it should have rejected, because it let the token influence how it was verified or because the verification was weak or absent. So prevention is mostly about taking that control back. Pin how you verify, use a key that cannot be guessed or reused, and ignore anything the token tries to say about its own key. This page covers the principles and the concrete code. Build the habit on a free live target.
Pin the algorithm, always
The single most important rule: the server decides the algorithm, not the token. Decide which algorithm you accept and pass it explicitly to the verifier, rejecting everything else, including none. This one control closes both alg:none and algorithm confusion at once, because neither works if the token's alg header cannot change the verification path.
In Python with PyJWT, pass an explicit algorithms list on decode. PyJWT makes you pass it when verifying, and it rejects any token whose algorithm is not in the list:
import jwt
claims = jwt.decode(
token,
key,
algorithms=["HS256"], # pin it; a token claiming none or RS256 is rejected
)In Node with the jsonwebtoken library, set algorithms in the verify options:
const jwt = require("jsonwebtoken");
const claims = jwt.verify(token, key, { algorithms: ["HS256"] });
// a token with a different alg, or none, throws instead of verifyingNever build the algorithms list from the incoming token, and never pass a list that mixes HMAC and RSA algorithms with a key that could serve both, which is what makes confusion possible.
Verify, do not just decode
Decoding a token reads its claims. Verifying checks the signature. They are different calls, and using the decode one for authentication is the "server never verifies" bug. Always call the verifying function, provide the key, and treat a verification failure as a hard reject. Fail closed: if you cannot verify, deny the request rather than falling through to trust the claims.
Reading claims without verifying is fine only when you genuinely do not trust them yet, for example to read the kid before you look up a key, and even then the claims must not drive any security decision until the signature checks out.
Use strong keys
- For HMAC (
HS256), use a long, random secret, at least 256 bits from a cryptographic random source. Never a human-chosen word, a tutorial default, or a secret reused from elsewhere. This is what defeats offline cracking. - For RSA or EC (
RS256,ES256), manage keys properly: protect the private key, rotate on a schedule, and never expose signing material. The public key being public is fine, as long as the server pins the algorithm so that public key can never be used as an HMAC secret.
Do not trust attacker-controlled headers
The key-selection headers are the whole header-injection attack surface. Lock them down:
kid: if you use it to select a key, map it through a strict allow-list of known ids. Never let it become a file path or a raw database query, which is what enables traversal and SQL injection.jwk,jku,x5u,x5c: reject them, or pin them to a fixed, server-controlled source. A session token should not carry or point at its own verification key. If you fetch a JWKS, fetch it from a URL you configured, not one the token supplies.
Validate the claims
A valid signature only proves the token is authentic, not that it is still appropriate. On every request, check:
expso expired tokens are rejected. Many "replay an old token" bugs are simply a missing expiry check.nbfso a token is not used before its valid window.issandaudso a token minted for another issuer or audience is not accepted here.
Keep tokens short-lived, and support revocation (a deny-list or short rotation) rather than trusting a long-lived stateless token you can never recall.
A short checklist
- Pin the accepted algorithm server-side and pass it explicitly to the verifier. Reject
none. - Call the verify function, provide the key, and fail closed on any verification error.
- Use a long random HMAC secret, or well-managed asymmetric keys. Never reuse a key across algorithms.
- Reject or pin
jwk,jku,x5u, andx5c. Allow-listkidand never let it reach a file or query sink. - Validate
exp,nbf,iss, andaudon every request. Keep tokens short-lived and revocable.
That is the whole defense. For the offensive side in one place, see the JWT cheat sheet, and to prove to yourself that pinning the algorithm really does close the bug, break an unpinned one on a free live target.