JWT Security: Common Vulnerabilities and Best Practices
JSON Web Tokens are everywhere: API authentication, single sign-on, mobile apps and service-to-service calls. They are also easy to get wrong, and a JWT mistake often means anyone can log in as anyone.
How a JWT works
A JWT has three base64url-encoded parts separated by dots: a header, a payload and a signature.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciJ9.signature
The payload is readable by anyone. The signature only proves the token was not modified, and only if the server verifies it correctly.
Vulnerability 1: Not verifying the signature
Some code decodes the token and trusts the payload without verifying. In many libraries, decode() does not verify, while verify() does. An attacker edits "role":"user" to "role":"admin" and is now an admin.
Vulnerability 2: The none algorithm
The JWT standard defines alg: none for unsigned tokens. Older or misconfigured libraries accept tokens with alg: none and no signature. Fix: explicitly specify the allowed algorithms when verifying.
jwt.verify(token, key, { algorithms: ["RS256"] });
Vulnerability 3: Algorithm confusion
If a server expects RS256, which uses a public and private key pair, but the library also accepts HS256, which uses a shared secret, an attacker can sign a token with HS256 using the public key as the secret. The server then verifies it with the same public key and accepts it. Fix: pin the algorithm.
Vulnerability 4: Weak HMAC secrets
Tokens signed with HS256 and a weak secret like secret or changeme can be cracked offline from a single captured token. Fix: use a long random secret, at least 256 bits, stored in a secrets manager, or use asymmetric keys.
Vulnerability 5: Header injection via kid, jku and x5u
These header fields tell the server which key to use. If the server trusts them without restriction, attackers can point to their own key, or use kid for path traversal or SQL injection in the key lookup. Fix: ignore jku and x5u unless you allowlist URLs, and treat kid as an untrusted lookup key.
Vulnerability 6: Missing claim validation
Even a valid signature is not enough. Validate:
exp, the expiry time.nbf, not before.iss, the issuer.aud, the audience, so a token for one service cannot be replayed against another.
Vulnerability 7: Sensitive data in the payload
Because payloads are readable, never put secrets, passwords or sensitive personal data in a JWT.
Best practices summary
| Practice | Why |
|---|---|
| Always verify, never just decode | Prevents forged tokens |
| Pin allowed algorithms | Blocks none and confusion attacks |
| Strong secrets or asymmetric keys | Prevents offline cracking |
| Validate exp, iss, aud | Prevents replay and misuse |
| Short expiry plus refresh tokens | Limits stolen token impact |
| Plan for revocation | JWTs are hard to revoke otherwise |
Testing JWT handling
SecurityInfinity agents capture tokens during authenticated testing and check for unsigned acceptance, algorithm confusion, weak secrets, header injection and missing claim checks. Start free.
Frequently asked questions
Are JWTs encrypted?
Standard signed JWTs are not encrypted. Anyone can read the payload. JWE is a separate standard for encrypted tokens.
Where should JWTs be stored in the browser?
HttpOnly cookies are safer against XSS than localStorage. If you store tokens in JavaScript-accessible storage, keep them short-lived.
How do you revoke a JWT?
Use short expiry, maintain a denylist of revoked token IDs, or check a per-user session version on each request.