JWT Security Pitfalls: What Attackers Look For
JWTs are easy to use and even easier to misuse. The five mistakes I look for first when I see a Bearer token, and how to fix each one.
JWTs are everywhere because they are easy: drop a library, sign a blob, ship it. The problem is that the failure modes are not in the library — they are in how your app uses the library. When I see a Bearer eyJ... flying across the wire on an engagement, the first thing I do is decode it and walk down a checklist. Here is what is on that checklist and why each item earns a place.
Pitfall 1: trusting the alg claim
The classic. Every JWT has an alg header that tells the verifier which algorithm to use. If your code reads that field and dispatches accordingly, an attacker sends {"alg":"none"}, the verifier dutifully skips signature validation, and any token is now valid. The variant is alg: HS256 against an RS256 key — the verifier uses the public key as the HMAC secret, and the attacker (who has the public key) forges tokens at will.
The fix is the same in both cases: pin the algorithm on the verifier side, do not read it from the token. Most libraries support this with an option like algorithms: ['RS256']. If yours doesn't, switch libraries.
Pitfall 2: weak HMAC secrets
HS256 is symmetric, which means your signing secret is also your verifying secret. If that secret is secret, changeme, the name of the project, or anything under twelve characters, an attacker who captures one token can brute-force the secret offline in seconds and then forge anything. There are public wordlists of millions of leaked JWT secrets — yours is probably on one of them.
Use a 256-bit random secret stored in a real secret manager. If you can't do that, switch to RS256 so the signing key never leaves the issuing server.
Pitfall 3: missing or weak exp validation
A surprising number of apps issue tokens with an exp claim and then never check it. Or they check it but accept tokens with no exp at all, treating "no expiry" as "never expires" instead of "reject". Both are exploitable: capture one token in any way, and it works forever.
Validate three things: exp exists, exp is in the future, and iat is not absurdly far in the past. Reject tokens older than your refresh window even if the signature is valid.
Pitfall 4: storing JWTs in localStorage
This is the religious-war one, so I will be brief. localStorage is reachable from any JavaScript that runs on your origin, including the XSS that you swear cannot happen. Once an attacker has the token, they have the user — refresh flow and all. HttpOnly secure SameSite=strict cookies are not a perfect defence but they remove the easy XSS-to-token-exfiltration path, which is the one that actually gets exploited.
If you must use localStorage because of SPA architecture, the mitigation is short-lived tokens (5–15 minutes) plus a refresh token stored in a cookie, plus a CSP strict enough that you genuinely cannot get XSS. Two of those three are not enough.
Pitfall 5: trusting the sub or email claim from a third party
When you accept a token from another service — SSO, OAuth, anything federated — the claims inside are only as trustworthy as your validation of the signature and the issuer. I have lost count of engagements where the app verified the signature against the right key but never checked iss or aud, so any signed token from any tenant of the IdP was accepted. Tokens minted for tenant-a worked against tenant-b. Whoops.
Validate iss against an allowlist. Validate aud is your service. Reject any token where either is missing.
How to actually look at the thing
Most JWT bugs are visible in 30 seconds if you decode the token. Paste it into our JWT decoder and read the header and claims; you can see immediately whether alg is sane, whether exp is set, whether iss and aud are filled in. For testing the secret strength, pair it with a quick run through our hash identifier on the signature segment — if the surrounding signing flow uses HMAC with a weak password, john or hashcat will tell you in under a minute.
The fixes for all five pitfalls combined take less than a day for any team that has access to the auth layer. The cost of leaving them in is usually total account takeover. The math is not subtle.
Related articles
Auditing a WordPress Site in 30 Minutes
WordPress runs 40 % of the web. Most of those installs have at least one critical issue. Here is the half-hour audit that finds them.
Security Headers: The Five That Actually Matter
Most security-header guides list twenty. Here are the five that actually change attacker behaviour, with the misconfigurations I see weekly.
What Is SQL Injection and How It Works
A practical walkthrough of SQL injection — what causes it, how attackers exploit it, and the parameterised-query pattern that kills it for good.