JWT Security: Seven Mistakes That Break Token Authentication
Seven JSON Web Token mistakes that let attackers forge or reuse tokens, from alg none and key confusion to weak secrets, and the RFC 8725 fixes.
By MD-5 5 min read
On this page
JSON Web Tokens (JWTs) carry the identity of the user in almost every modern API. The format is sound. The problems come from how applications create and check tokens, and they are serious ones: most of the mistakes below let an attacker log in as any user.
The IETF published a best-practice document for exactly this reason. RFC 8725, JSON Web Token Best Current Practices (opens in a new tab), was written after the same implementation flaws kept recurring across libraries. This article walks through the seven we see discussed most, with the fix for each.
A 30-second refresher
A signed JWT has three base64url-encoded parts separated by dots: a header (which algorithm signed it), a payload (the claims, such as user ID, expiry and audience) and a signature.
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIyMDciLCJyb2xlIjoidXNlciJ9 . 4pcPyMD09olPSyXnrXCjTwXyr4BsezdI1AVTmud2fU4
header payload signature
The server trusts the payload only because the signature checks out. Every mistake below is a way of trusting the payload without a valid signature, or of trusting a valid signature for longer or wider than it should.
1. Accepting alg: none
The JWT specification (RFC 7519 (opens in a new tab)) defines an “unsecured” token whose header says "alg": "none" and whose signature is empty. Some libraries used to honor that header, so an attacker could edit the payload, set alg to none, strip the signature and be accepted.
Fix: configure the verifier with the exact algorithm or algorithms you use, and reject everything else. Never let the token’s own header choose how it is verified. RFC 8725 section 3.1 says libraries must let callers specify the permitted algorithms.
2. Algorithm confusion (RS256 to HS256)
With RS256 the server signs with a private key and verifies with a public key. With HS256 one shared secret does both. If a server expects RS256 but its library picks the algorithm from the token header, an attacker can send a token marked HS256 and signed with the server’s public key used as the HMAC secret. The public key is public, so the attacker can forge any token. This exact flaw was fixed in the popular Node.js jsonwebtoken library as CVE-2015-9235 (opens in a new tab).
Fix: same as above. Pin the algorithm per key, and use a library version that separates key types so a public key can’t be used as an HMAC secret.
3. Weak HMAC secrets
An HS256 token is only as strong as its secret. Because the attacker has a valid token (their own), they can guess secrets offline as fast as their hardware allows, with no rate limiting and no alerts. Password crackers support this directly (hashcat mode 16500). Secrets like secret, the app’s name, or a value copied from a tutorial fall in seconds, and a cracked secret lets the attacker sign tokens for any user.
Fix: use at least 256 bits of cryptographically random data as the secret, stored in a secrets manager rather than in code. For systems where many services verify tokens, prefer asymmetric signing (ES256 or EdDSA) so verifiers never hold a signing key.
4. Not validating the claims
A valid signature proves who issued the token, not that this service should accept it now. Common gaps:
- Expiry (
exp) not checked, so stolen tokens work forever. - Audience (
aud) not checked, so a token issued for one service is accepted by another that trusts the same issuer. RFC 8725 section 3.9 covers this cross-service substitution. - Issuer (
iss) not checked where several identity providers are trusted. - Decoding instead of verifying. Most libraries have a
decode()function that reads the payload without checking the signature. Using it for authorization is a surprisingly common bug.
Fix: verify signature, exp, nbf, iss and aud on every request, through the library’s verify function, with a small clock-skew allowance.
5. Trusting key hints in the header
Headers such as kid (key ID), jku (URL of a key set) and x5u (URL of a certificate) tell the server which key to use. If the server follows them without restriction:
- A
jkuorx5upointing at an attacker’s server makes the application fetch the attacker’s key and verify the attacker’s signature. - A
kidused in a file path or database query can be abused for path traversal or SQL injection, for example to make the server use a predictable file as the key.
Fix: treat header values as untrusted input. Resolve kid against an allow-list of your own keys, and ignore jku and x5u unless they match exact, pre-configured URLs.
6. Putting secrets in the payload
A signed JWT is encoded, not encrypted. Anyone holding the token can read the payload with a base64 decoder. Tokens end up in logs, browser storage, analytics tools and support tickets, so anything in them should be treated as visible.
Fix: keep payloads to identifiers and authorization claims. If a token genuinely must carry confidential data, use an encrypted JWT (JWE) or, better, keep the data server-side and look it up.
7. Tokens that can’t be revoked
JWTs are usually stateless: the server doesn’t store them, so it can’t easily cancel one. Combine that with a 30-day lifetime, and a token stolen through cross-site scripting, a leaked log or a compromised device stays valid long after the user logs out or changes their password.
Fix: keep access tokens short-lived (minutes, not days) and pair them with refresh tokens that are stored server-side, rotated on use and revoked on logout or password change. Keep a deny-list for the rare cases you must kill an access token early.
Library bugs happen too
Even correct application code depends on the library getting verification right. In 2022, Java versions 15 to 18 were found to accept ECDSA signatures made of all zeros, meaning any ES256 token “verified” regardless of the key (CVE-2022-21449 (opens in a new tab), nicknamed “psychic signatures”). Keep JWT and crypto libraries on your normal patch cycle, and include a forged-signature test in your automated test suite so a regression is caught.
A checklist for your API
- The verifier is configured with an explicit algorithm list;
noneis rejected. - Each key is bound to one algorithm; public keys can’t be used as HMAC secrets.
- HMAC secrets are 256 or more random bits, stored in a secrets manager.
exp,nbf,issandaudare validated on every request.kid,jkuandx5uare resolved against allow-lists only.- Payloads contain no personal or secret data.
- Access tokens expire within minutes; refresh tokens are revocable.
- Tests exist for an unsigned token, a token signed with the wrong key, an expired token and a token for another audience.
Token handling is a standard part of an API penetration test: each of the attacks above is tried against your real endpoints. For the authorization flaws that often sit behind a correctly verified token, see broken access control: IDOR and BOLA.
Sources
- IETF, RFC 8725: JSON Web Token Best Current Practices (opens in a new tab), February 2020.
- IETF, RFC 7519: JSON Web Token (JWT) (opens in a new tab) and RFC 7515: JSON Web Signature (JWS) (opens in a new tab).
- NIST NVD, CVE-2015-9235 (opens in a new tab) (algorithm confusion in node-jsonwebtoken).
- NIST NVD, CVE-2022-21449 (opens in a new tab) (ECDSA signature verification in Java).
- OWASP, JSON Web Token Cheat Sheet (opens in a new tab).
Published by
MD-5 · Certified cybersecurity services
Articles are researched and written in-house and link to their primary sources (standards, advisories, papers and public datasets) so you can check every claim. Spotted an error? Email [email protected] and we’ll correct it.