July 11, 2026
JWT Security: Common Vulnerabilities and How to Test Them
Learn how to identify, test, and prevent the most common JWT vulnerabilities with practical techniques used during real-world pentesting

By Daniel Nweke
6 min read
One of the most critical findings I've ever reported came from a vulnerability I almost ignored.
After hours of testing, I was convinced the application was solid. Then I looked at its JSON Web Token one more time. I changed a single value in the header, didn't re-sign the token, and sent it back.
The API accepted it without hesitation.
No sophisticated exploit. No chained vulnerabilities. Just an application that trusted a JWT far more than it should have.
That's the thing about JWT vulnerabilities. The format isn't usually the problem. The way applications validate tokens is.
JSON Web Tokens have become the de facto standard for authentication in modern web and mobile applications. They're fast, stateless, and widely supported.
But a single mistake in how an application generates, validates, or processes them can turn a secure authentication mechanism into an attacker's easiest path to privilege escalation or account takeover.
In this article, we'll examine some of the most common JWT vulnerabilities you'll encounter during security assessments, why they happen, and, more importantly, how to test for them responsibly.
What a JWT actually is
Before picking one apart, it helps to see what you're actually looking at.
A JWT is three base64url-encoded segments joined by periods: header.payload.signature. Nothing in the header or payload is encrypted. It's just encoded, which means anyone can read it without a key. Try it yourself with any JWT you have lying around:
echo -n "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# {"alg":"HS256","typ":"JWT"}echo -n "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
# {"alg":"HS256","typ":"JWT"}Do the same with the second segment (the payload), and you'll see the claims: things like sub (subject), role, exp (expiration), and whatever else the developer decided to include.
The third segment is the signature, which is proof that the first two segments haven't been tampered with, assuming the server actually checks it properly.
You can decode all three parts faster with a purpose-built tool instead of doing it segment by segment:
python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0aWNhcnBpIn0.abc123python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0aWNhcnBpIn0.abc123jwt_tool reads the header and payload and prints them out clearly, which is where every JWT assessment should start: understanding what the token is claiming before you try to break anything.
What problem do JWTs actually solve?
Traditional session management requires the server to store session state somewhere: in a database, a cache, or memory. That works fine for a single application but gets awkward once you have multiple services, mobile clients, and APIs that need to trust each other without a shared session store.
JWTs solve this by putting the "session" inside the token itself. Any service holding the right key can verify the signature and trust the claims inside without ever touching a database. It's a genuinely useful design for distributed systems.
The catch is that this convenience shifts all the trust onto cryptographic verification. If that verification step has a flaw, nothing else is checking whether the claims are legitimate.
The "none" algorithm
The JWT header declares its own signing algorithm, and the spec includes a legitimate value called none, meant for cases where integrity is guaranteed some other way. Some libraries, especially older ones, honored whatever algorithm the header claimed rather than the algorithm the server expected.
That means an attacker can decode a token, change the header to alg: none, strip the signature entirely, and modify the payload freely, including bumping their own role from user to admin. If the server's verification code doesn't explicitly reject none, it accepts the token as valid.
jwt_tool has this built in as an exploit mode:
python3 jwt_tool.py YOUR_JWT_HERE -X apython3 jwt_tool.py YOUR_JWT_HERE -X aThat single flag rewrites the header with each casing variant of none (none, None, NONE), strips the signature, and hands you ready-to-send tokens. Replay the request with each one and see if any get accepted.
Algorithm confusion between RS256 and HS256
This one is subtler and, honestly, my favorite to explain.
Some applications use asymmetric signing (RS256), where the server signs tokens with a private key and verifies them with a public key that can be shared freely, and sometimes it's even published for other services to fetch.
The vulnerability shows up when the server's verification logic doesn't pin the expected algorithm. If an attacker can get a token verified using HMAC (HS256) instead, and the server's public key is known, the attacker can sign a forged token using that public key as the HMAC secret.
The server, expecting RS256 but not enforcing it, ends up verifying a token that was "signed" with information it published itself.
First, find the public key. If the app reuses its TLS certificate for signing, you can often pull it straight from the connection:
openssl s_client -connect target.example.com:443 </dev/null 2>/dev/null | openssl x509 -pubkey -noout > public.pemopenssl s_client -connect target.example.com:443 </dev/null 2>/dev/null | openssl x509 -pubkey -noout > public.pemThen let jwt_tool do the key-confusion attack directly:
python3 jwt_tool.py YOUR_JWT_HERE -X k -pk public.pempython3 jwt_tool.py YOUR_JWT_HERE -X k -pk public.pemIf the resulting token gets accepted, the algorithm isn't being pinned server-side, and you've just turned a public key into a forging key.
Weak or guessable signing secrets
Symmetric algorithms like HS256 are only as strong as the secret behind them.
Plenty of applications use short, predictable, or leaked secrets, sometimes even a default value from a tutorial that never got changed.
Because verification is a self-contained cryptographic operation, cracking the secret is entirely offline. You don't need to touch the live application at all, which makes it one of the lowest-risk, highest-value things to try early.
jwt_tool can run a dictionary attack directly:
python3 jwt_tool.py YOUR_JWT_HERE -C -d wordlist.txtpython3 jwt_tool.py YOUR_JWT_HERE -C -d wordlist.txtFor high speed, hand it to hashcat instead, which has a dedicated JWT mode:
hashcat -a 0 -m 16500 jwt.txt wordlist.txthashcat -a 0 -m 16500 jwt.txt wordlist.txtMode 16500 understands the JWT structure directly; no manual hash formatting needed. On a decent GPU, this can chew through common wordlists in seconds. If it finds a secret, you can now sign any payload you want, with any claims you want.
Header injection through kid or jku
Some implementations include a kid (key ID) or jku (JWK Set URL) field in the header, telling the verifier which key to use. If that value isn't strictly validated, it becomes an injection point. A kid used unsanitized in a filesystem lookup can be walked with path traversal toward a file with predictable contents, then that content gets used as the signing key:
python3 jwt_tool.py YOUR_JWT_HERE -I -hc kid -hv "../../../../dev/null" -S hs256 -p ""python3 jwt_tool.py YOUR_JWT_HERE -I -hc kid -hv "../../../../dev/null" -S hs256 -p ""A jku pointing to an attacker-controlled URL can trick the server into fetching a key you generated yourself, then verifying a token signed with it — jwt_tool's -X s mode automates spoofing the JWKS location for exactly this test. Either way, the lesson is the same: inspect every header field, not just the payload. The claims people forget to scrutinize are usually the ones nobody validates.
Missing or ignored expiration and audience checks
A JWT can carry exp, nbf, aud, and iss claims, but including them means nothing if the server doesn't enforce them. I've seen tokens accepted well past their expiration, and tokens issued for one service accepted by a completely different one because nobody checked the aud claim.
Testing this needs no tooling at all; just capture a token, let it expire, and resend it.
Try a token issued for one service against a different service in the same ecosystem. Both requests should fail. Often they don't.
Sensitive data sitting in plain view
Because JWTs are encoded, not encrypted, anyone holding the token can read the payload, including any personal data, internal identifiers, or role information a developer assumed was hidden.
This isn't always exploitable directly, but it's frequently worth flagging, and it can hand an attacker useful reconnaissance for other attacks. A quick base64 -d on the payload segment is often the fastest recon you'll do all day.
Where storage matters as much as the token
Even a perfectly implemented JWT can be undone by where it's stored.
Tokens sitting in localStorage are reachable by any JavaScript running on the page, which turns a single cross-site scripting flaw into full session theft. httpOnly cookies aren't perfect either, but they remove that particular path entirely.
A quick mental checklist
When I sit down with a new JWT-based application, this is roughly the order I work through, and it maps cleanly onto everything above:
- Decode the header and payload first. What algorithm, what claims, what does the app assume you can't see?
- Try
alg: none. It costs nothing and occasionally works. - If it's RS256, look for the public key — TLS certs, JWKS endpoints, API docs — and try key confusion.
- If it's HS256, throw a wordlist at it offline before doing anything else risky.
- Check every header field, not just
alg.kidandjkuare often the least-tested part of the token. - Let a token expire and replay it. Try it against a sibling service if one exists.
- Note where the token lives in the browser, because that decides whether an unrelated XSS becomes a full account takeover.
None of these steps requires the target application to be fragile in some exotic way.
They just require someone to have trusted the token a little more than they verified it, which, in my experience, is the default state of most JWT implementations until someone checks.
The trade-off worth remembering
JWTs aren't insecure by design. They are a shift of responsibility: from "does the server remember this session" to "did the server verify this signature correctly, using the algorithm it expected, with a secret nobody could guess."
Every vulnerability above is really the same root cause wearing a different outfit: verification logic that trusts the token more than it checks it.
That's the question worth asking on every engagement, and honestly, on every code review too: not "is this a JWT?" but "what exactly is verifying it, and what happens if that check is wrong?"
The afternoon I mentioned at the start only turned interesting once I stopped assuming the server was doing its job and started checking.
Disclaimer:_ The techniques discussed in this article are intended for educational purposes and authorized security assessments only. Always ensure you have explicit permission before testing any application or system._