JWT security: vulnerabilities and how to test them
JSON Web Tokens are everywhere in modern applications. They power stateless authentication across APIs, single-page apps, and microservices, letting a server verify who a user is without storing session state. That convenience is exactly why JWT bugs are so valuable: when the token is the proof of identity, a flaw in how it is validated can mean full authentication bypass. Change one field, become the admin.
This guide explains how JWTs work, the vulnerabilities that show up again and again in real applications, and, most importantly, how to test for them with actual commands. It pairs naturally with the broader API security testing guide.
Practise legally: every test below assumes a token from a system you own or are explicitly authorised to test. The best free place to build these skills is PortSwigger’s Web Security Academy, which has a full set of JWT labs you are allowed to break. On a real engagement, stay strictly inside your authorised scope. Forging a token against a login you have no permission for is unauthorised access, plain and simple.
How a JWT works
The format is standardised in RFC 7519. A JWT is three Base64URL-encoded parts joined by dots:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoxMDQyLCJyb2xlIjoidXNlciJ9.KmA3...signature
header payload signature
- Header: JSON describing the token, most importantly the signing algorithm (
alg), for example{"alg":"HS256"}. - Payload: JSON containing the claims: who the user is, their role, when the token expires, for example
{"user":1042,"role":"user","exp":1720000000}. - Signature: a cryptographic signature over the header and payload, using either a shared secret (HMAC, like HS256) or a private key (asymmetric, like RS256).
Here is the point most beginners miss: the header and payload are only encoded, not encrypted. Anyone with the token can decode and read them. Paste one into jwt.io and every claim is immediately legible. The signature does not hide the contents, it only proves they have not been changed. The server trusts a JWT because it can recompute and verify the signature. Break or bypass that verification and the whole trust model collapses.
The two root causes of almost every JWT bug
Nearly every JWT flaw comes down to one of two failures: the signature is not verified properly, or the claims are not validated properly. Keep those two buckets in mind and the specific bugs below stop feeling like a random list and start feeling like variations on a theme.
Signature failures
alg: none. The spec includes anonealgorithm, meaning no signature at all. If a server accepts{"alg":"none"}and skips verification, an attacker rewrites the payload (say,rolefromusertoadmin), drops the signature, and sends it. A vulnerable server trusts it.- Weak HMAC secret. Tokens signed with HS256 depend entirely on a shared secret. If that secret is a dictionary word or a short default, anyone who captures one valid token can brute-force it offline and then sign any token they want.
- Algorithm confusion (RS256 to HS256). In RS256, tokens are signed with a private key and verified with a public key that is not secret. If the verifier lets the token’s header pick the algorithm, an attacker switches
algto HS256 and signs with the public key as the HMAC secret. A naive server validates the forgery because it uses that same public key as the HMAC key. - Signature simply not checked. Sometimes a developer decodes the payload to read claims but forgets to verify the signature first. The token becomes attacker-editable JSON.
Claim failures
- No expiry check. A token with no
exp, or a server that ignores it, means a stolen token never stops working. - Missing audience or issuer checks. If the server does not validate
audandiss, a token issued for one service or environment may be accepted by another that shares a key. - Sensitive data in the payload. Because the payload is readable by anyone, putting secrets, API keys, or personal data in claims is a straight disclosure bug. Treat the payload as public.
kidheader injection. The optionalkid(key ID) header tells the server which key to use. If the server uses that attacker-controlled value unsafely, to build a file path or run a query, it can become a vector for path traversal or SQL injection.
How to test JWT security: three worked attacks
Reading about these is one thing. Running them is what makes the flaw click. First, find the token. JWTs usually ride in the Authorization: Bearer ... header, but they also hide in cookies, in localStorage, and in request bodies, so watch the traffic in Burp Suite while you log in and note every place a dotted eyJ... string appears. Any of them is a candidate to test.
The go-to tool is jwt_tool (ticarpi/jwt_tool), which decodes, tampers, forges, and cracks tokens. Install it, grab a valid token from your authorised target, and start by just decoding it:
python3 jwt_tool.py eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoxMDQyLCJyb2xlIjoidXNlciJ9.KmA3...
Token header values:
[+] alg = "HS256"
[+] typ = "JWT"
Token payload values:
[+] user = 1042
[+] role = "user"
[+] exp = 1720000000
Now you know the algorithm and the claims. That tells you which of the three attacks to try.
Attack 1: forge an unsigned token (alg:none)
The fastest thing to check is whether the server accepts an unsigned token. jwt_tool’s exploit mode builds the none variants for you:
python3 jwt_tool.py <TOKEN> -X a
[+] alg:none forged token (variant "none"):
eyJhbGciOiJub25lIn0.eyJ1c2VyIjoxMDQyLCJyb2xlIjoidXNlciJ9.
To make yourself an admin, run tamper mode (-T) first, change role to admin, then re-issue with the none algorithm. Send the forged token in place of the real one and watch the response. If the server treats you as an admin, the signature is not being verified. That is a critical finding.
Attack 2: crack a weak HMAC secret
If the token uses HS256, its whole security rests on the secret. Put the full token in a file and let hashcat run a wordlist against it in JWT mode:
echo '<TOKEN>' > jwt.txt
hashcat -m 16500 -a 0 jwt.txt /usr/share/wordlists/rockyou.txt
-m 16500 is hashcat’s JWT (HMAC) mode. If the secret is weak, it falls out in seconds:
<TOKEN>:secret123
Now you own the signing key. You can mint a token with any claims you like and it will validate perfectly, because it is signed with the real secret:
python3 jwt_tool.py <TOKEN> -T -S hs256 -p "secret123"
This is why HMAC secrets must be long, random, and unguessable. rockyou.txt should never contain your signing key.
Attack 3: algorithm confusion (RS256 to HS256)
This is the subtle one, and the highest-impact when it works. If the app signs with RS256, the public key is meant to be shared, and it is often published at a JWKS endpoint. Grab it:
curl -s https://target.com/.well-known/jwks.json -o jwks.json
Convert that JWK to a PEM public key (jwt_tool ships helpers for this, or use any JWK-to-PEM converter), then run the key-confusion exploit, which re-signs the token as HS256 using that public key as the HMAC secret:
python3 jwt_tool.py <TOKEN> -X k -pk public.pem
If the server was written to “verify with the public key” without pinning the algorithm, it validates the forged HS256 token, because it uses the same public key as the HMAC key. From there you forge any user, including administrators.
Attack 4: abuse the header (kid and jku)
Two optional header fields are worth probing, because they tell the server where to find its key. The kid (key ID) header points at which key to load. If the server feeds that value into a file path or a database query without sanitising it, you can redirect verification at something you control: a path-traversal kid can point at a predictable file whose contents you already know (so you sign with that value), and a SQL-injectable kid can be coerced into returning a key you chose.
The jku (JWKS URL) header is even more direct. It tells the server where to fetch the public keys. If the server trusts an attacker-supplied jku, you host your own key set at a URL you control, point the token at it, and sign with your matching private key. jwt_tool automates both the inline-JWKS injection and the spoofed-URL attack:
python3 jwt_tool.py <TOKEN> -X i # inject an inline JWKS into the token header
python3 jwt_tool.py <TOKEN> -X s # spoof a JWKS hosted at a URL you control
The rule of thumb: any header field that decides which key verifies the token is a field worth attacking.
Also check the boring stuff
Between those three, work the checklist: does an expired token still work? Does a token with a wrong aud or iss get accepted? Does changing a claim without touching the signature get through (proving the signature is not checked)? And if a kid header is present, test it for injection. The exotic attacks get the attention, but a missing expiry check is a real finding too.
Where to practise
Do not learn any of this for the first time on a live target. PortSwigger’s Web Security Academy has a dedicated set of free JWT labs covering exactly these attacks: unverified signatures, alg:none, weak secrets, and algorithm confusion. Every lab is legal to break, and working through them with jwt_tool is the fastest way to build the instinct for spotting the non-obvious cases.
How to secure JWTs
Robust JWT security is mostly strict configuration and using a vetted library correctly. The single most important rule ties every attack above together: pin the algorithm and verify before you trust. Decide the exact algorithm your app uses, reject everything else including none, and never let the token’s own header choose how it is verified.
In practice, that means configuring your library explicitly rather than accepting its defaults. Here is what a strict verification looks like in Python with PyJWT:
import jwt
def verify(token: str) -> dict:
return jwt.decode(
token,
key=PUBLIC_KEY,
algorithms=["RS256"], # reject none, HS256, and everything else
audience="my-api", # validate aud
issuer="https://auth.example", # validate iss
options={"require": ["exp", "iat"]}, # force expiry to be present and checked
)
Around that core, apply the rest:
- Use strong keys and secrets: long, random HMAC secrets, and properly managed private keys for asymmetric signing.
- Validate all claims, not just the signature:
exp,nbf,aud, andisson every request. - Keep tokens short-lived: pair short access-token lifetimes with refresh tokens and a revocation strategy, since a plain JWT cannot be invalidated early.
- Never store secrets in the payload. Treat it as public.
- Protect the transport: always send tokens over HTTPS so they cannot be intercepted. Verify your TLS setup with the SSL/TLS Checker.
One connection worth noting: because JWTs are often sent in an Authorization header rather than a cookie, cookie-based attacks like CSRF usually do not apply to them. But that only holds if you do not store the token in a cookie. Your storage choice changes the threat model.
Key takeaways
- A JWT’s header and payload are encoded, not encrypted, and readable by anyone. The signature only proves integrity.
- Almost every JWT bug is a signature failure (
alg:none, weak secret, algorithm confusion, skipped verification) or a claim failure (missing expiry, audience, or issuer checks). - Test with jwt_tool and hashcat: forge an
alg:nonetoken, crack a weak HS256 secret, and try RS256-to-HS256 confusion against the published public key. - Secure JWTs by pinning the algorithm and verifying before trusting, using strong keys, validating every claim, and keeping tokens short-lived.
When the token is the identity, its verification logic is the lock on your entire application. Get it right and JWTs are a clean way to do stateless auth. Get it wrong and it is a single header change away from admin.
For implementation guidance, the OWASP JSON Web Token Cheat Sheet covers every validation step your library should be doing, and if your tokens are issued through OAuth 2.0, RFC 6749 is the spec that governs the flow. For cracking weak HMAC secrets, Hashcat supports JWTs natively.
Continue with the API security testing guide and the IDOR and access control explainer. Build these skills hands-on with our training, and if you want your own API’s token handling reviewed properly, that is what our security assessments are for.