JWT: the 3 flaws I see in every pentest
JSON Web Tokens are everywhere: APIs, login, SSO. And they hide a mental trap: they look opaque, but a JWT is not encrypted, it is encoded. Anyone can read its contents without a key. That is not the flaw —it is by design— but it is the door the three implementation mistakes I keep running into walk through.
Anatomy in 20 seconds
A JWT is three Base64url parts separated by dots: header.payload.signature.
eyJhbGciOiJIUzI1NiJ9 . eyJ1c2VyIjoiYWRtaW4ifQ . <signature>
└─ {"alg":"HS256"} └─ {"user":"admin"} └─ HMAC/RSA
The header and payload are readable by anyone (just Base64). The only thing stopping forgery is the signature. All of the token's security rests on the server verifying it correctly. Fail there and it is over.
alg field.Flaw 1 — alg: none
The spec allows the none algorithm: an "unsigned" token. If the server accepts it, the attacker changes the header to {"alg":"none"}, drops the signature and rewrites the payload at will:
{"alg":"none"}.{"user":"admin","role":"admin"}.
With no signature check, the backend believes it. That is direct privilege escalation. The defense: never accept none, and do not let the token choose the algorithm.
Flaw 2 — weak HMAC secret (HS256)
With HS256 the signature is an HMAC using a shared secret. If that secret is weak (secret, changeme, a dictionary word), it breaks offline: capture a valid token and brute-force the signature (hashcat mode 16500, jwt_tool). Once the secret falls, you can sign any token: you are whoever you want to be.
Flaw 3 — algorithm confusion (RS256 → HS256)
With RS256 the token is signed with the private key and verified with the public one (which is, by definition, public). The classic bug: the attacker switches the header to HS256 and signs the token using the public key as the HMAC secret. If the server does not pin the algorithm and just uses "the key", it verifies with the public key as a symmetric secret… and validates the forged token. Its dumber cousin is the simplest bug of all: not verifying the signature at all (more common than you'd think in hand-rolled middleware).
And a reminder: the payload is not a secret
Since it is readable, putting sensitive data in the payload (emails, internal roles, IDs, third-party tokens) leaks it to anyone who captures the JWT. The payload has integrity if the signature is good, but it is never confidential.
How to harden it (server side)
- ✅ Always verify the signature, and pin the expected algorithm (do not read it from the token header).
- ✅ Reject
alg: noneexplicitly. - ✅ HS256: long, random secret (≥ 256 bits), rotated; no dictionary secrets.
- ✅ RS256: use the public key only to verify; make sure the library won't accept HS with that key.
- ✅ Validate
exp(andnbf/aud/iss); short lifetimes and refresh tokens. - ✅ Do not put secrets in the payload.
Triage checklist (offensive side)
- ✅ Decode the token and look at
algand the claims (JWT Decoder). - ✅ Try
alg: noneand an empty signature. - ✅ If HS256, try to crack the secret offline (weak/dictionary).
- ✅ If RS256, try the HS confusion with the public key.
- ✅ Tamper with claims (
role,user,exp) and watch whether the server truly verifies.
JWT is not insecure; almost always it is how it is verified that is. And since the token reads without a key, the first step —looking inside— is available to anyone. Make sure you take that look before an attacker does.