A JWT is not secure because it's a JWT
A JSON Web Token is just three Base64url-encoded JSON parts — header, payload, signature — joined by dots. Nothing about that format is secret or tamper-proof on its own; the *signature* is what makes a token trustworthy, and only if the server verifies it correctly. Almost every JWT breach comes down to a server that trusted a token it should have rejected. In 2026, several CVEs affecting cloud and enterprise systems still traced back to the same handful of validation mistakes below.
You can inspect any token's header and payload without the secret using a JWT decoder — which is the first hint that the payload is *not* confidential. Let's walk the classic attacks and exactly how to shut each one down.
Attack 1: the `alg: none` bypass
The JWT spec includes a "none" algorithm meaning *unsigned*. A naive library, told to accept whatever algorithm the token declares, will happily verify a token whose header says {"alg":"none"} and whose signature is empty — because "no signature" trivially matches "no algorithm."
An attacker takes a valid token, rewrites the header to alg: none, edits the payload to "role": "admin", drops the signature, and sends it:
{ "alg": "none", "typ": "JWT" }
{ "sub": "attacker", "role": "admin" }If your server accepts it, they're an admin. The fix: never let the token choose the algorithm. Configure your library with an explicit allowlist — algorithms: ["RS256"] — and reject everything else, none included.
Attack 2: RS256 → HS256 algorithm confusion
This is the subtle one. Asymmetric algorithms like RS256 sign with a *private* key and verify with a *public* key — and the public key is, by design, public. Symmetric HS256 uses one *shared secret* for both signing and verifying.
The attack: the server expects RS256 and holds the RSA public key to verify. An attacker crafts a token with the header changed to HS256, then signs it using the server's public key as the HMAC secret. If the server's verification code picks the algorithm from the token header, it will try to verify an HS256 token — and helpfully use the public key (which the attacker also has) as the HMAC key. The signature matches. Forged token accepted.
Server's real setup: verify( token, RSA_PUBLIC_KEY ) expecting RS256
Attacker's forged JWT: HS256, signed with RSA_PUBLIC_KEY as the secret
Vulnerable server: "header says HS256, I'll HMAC-verify with my key" ✅ forgedThe fix is the same principle as attack 1: pin the algorithm on the server. If you issue RS256, verify with algorithms: ["RS256"] only. Never accept multiple algorithm families on the same endpoint, and key your verification to the algorithm *you* chose, not the one the token advertises.
Attack 3: weak or leaked HMAC secrets
If you use HS256, the entire security of every token rests on one shared secret. A short, guessable, or committed-to-Git secret can be brute-forced offline: an attacker takes any valid token and tries candidate secrets until the signature recomputes. Dictionary attacks against weak JWT secrets are fast and well-tooled.
The fix: use a long, high-entropy secret (32+ random bytes, not a passphrase), keep it out of source control, and rotate it. For anything multi-service, prefer asymmetric RS256/ES256 so verifiers only ever hold the public key — see JWKS and key sets for distributing rotating public keys.
Attack 4: skipping the claims
Even a correctly-signed token is only trustworthy for what it actually asserts. Servers that check the signature but ignore the claims accept tokens that are expired, meant for another service, or issued by the wrong party. Always validate:
- `exp` — reject expired tokens (and
nbfif present). - `iss` — the issuer is one you trust.
- `aud` — the audience is *this* API, so a token minted for another service can't be replayed against yours.
The JWT validation checklist
Every one of the attacks above is stopped by the same disciplined verification:
- Pin the algorithm to an explicit allowlist; never trust the token's
alg, never allownone. - Use asymmetric keys (RS256/ES256) for anything spanning services, so verifiers hold only public keys.
- Use a strong, rotated secret if you must use HS256.
- Validate `exp`, `iss`, and `aud` on every request, not just the signature.
- Keep secrets out of the payload — the payload is readable by anyone; confirm it with the JWT decoder.
- Keep tokens short-lived and pair them with refresh tokens; see where to store JWTs.
For the fundamentals see JWT tokens explained and safe JSON handling. To experiment, build test tokens with the JWT builder and inspect them with the decoder.