What 500 automated pentests taught us about JWT handling
alg:none is dead, but weak secrets and unverified issuers are everywhere. The five JWT mistakes our AI finds most, ranked by real-world exploitability.
alg:none is dead, but weak secrets and unverified issuers are everywhere. After 500 automated pentests, we ranked the five JWT mistakes our AI finds most by how easily they turn into account takeover.
1. Algorithm confusion / JWT header manipulation
If your server does not verify that the alg field matches an expected list of algorithms, an attacker can replace a signed RSA token with one signed using HMAC and a secret they guess (or find in a public repo). The server trusts it because it only checks whether the signature is valid using that shared secret — it never notices the algorithm changed.
Remediation is simple: accept only the algorithms you expect (e.g. RS256) and reject any JWT whose alg is not on the whitelist.
2. Unverified issuer claims
Many APIs trust sub and role claims without verifying the entire signature chain. A common pattern is to only verify the iss but not the aud. An attacker who obtains an expired JWT from a different valid issuer can replay it — the API accepts the token because iss still matches and the signature is valid under the expected key.
3. Weak RSA keys and the default RSA fallback
We have detected dozens of APIs using 512-bit RSA keys (or the equivalent PS384 with weak padding). They look random until you factor them — and we've factored them in bulk with precomputed tables. The fix is a minimum 2048-bit key or, better, ECDSA P-256.
4. Unverified aud / audience validation
Even when the issuer is correct and the algorithm whitelist is enforced, missing aud validation lets an attacker reuse a token from an unrelated service. If your backend and frontend both issue JWTs from the same signing host, any frontend token will be accepted as a valid backend token.
Check the aud claim against exactly the audience value you expect. A prefix match is not enough.
5. No expiry or very long expiry
We see a lot of production services issuing tokens with exp values 30+ days out. No expiry means a leaked token is usable forever. Set exp to something short (e.g. 1 hour) and use refresh tokens with rotation. The user will hardly notice the extra round-trip.
What else we found
Beyond the common five, three patterns stood out:
keyidinjection: Some libraries allow an attacker-suppliedkidto influence which public key is used for verification. We've seen cases where an attacker setkidto a custom value and got the server to fetch a public key from a URL of their choosing. Only usekidvalues from a hard-coded mapping.- Missing key validation: Some implementations only check that the signature is valid against a key — not necessarily the expected one. This lets an attacker issue their own public/private key pair and sign tokens that pass verification. Always validate the key.
- Lack of refresh token rotation: When refresh tokens are reused, a stolen refresh token never expires unless you force rotation. Issue a new refresh token on every use and invalidate the old one. If the old refresh token appears again, you know it was leaked.