Understanding JWT Vulnerabilities: Common Pitfalls and How To Fix Them


As a developer, do you think your JWT implementation is secure? As a security engineer, can you spot a poor JWT implementation when reviewing a source code?
You’ll know the answer after reading this article.
JWTs can be deceptively simple, and one misstep in implementation can open the door to serious security flaws. This article walks you through the most common JWT weaknesses, how they are exploited, and what both developers and security engineers need to watch out for.
We’ll begin with a simple definition of JWT for readers who might not be familiar with it. We’ll also explain the parts of JWT, how vulnerabilities occur, common JWT vulnerabilities, and their recommended mitigations.
So, let’s jump right in!
If you're familiar with what JWT means, you can skip the definition.
What is JWT?
JSON web token (JWT), pronounced “jot”, is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. — Auth0
It’s commonly used for authentication and authorization, however, security is only achieved when JWT is correctly implemented and validated. Typically, the token is issued after a successful login and stored on the client side, either in the cookie or the Authorization header. Each time a user makes a request, the token is sent to the server, which verifies it using a secret key. If the token is valid, the server processes the request, otherwise the access is denied.
The Structure:
A JWT consists of three (3) parts:
Header
Payload
Signature
Each part of the token is base64url encoded and is separated by a dot (.)
The Header
This is the first part of the token. It contains the algorithm (alg) and the token type (typ). An encoded header looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
When decoded, this is what you see:
{
"alg": "HS256",
"typ": "JWT"
}
From the header, you can tell what algorithm is used to sign the token. Commonly used algorithms for signing tokens are HS256 (HMAC + SHA256) and RS256 (RSA + SHA256). HS256 uses a symmetric key, which means it uses a single key to sign and verify the token, while RS256 uses an asymmetric key pair, where a private key is used to sign and a public key is used to verify the token.
The Payload
This is where the user data is stored. It can include the username, user ID, role, etc., and it also includes the expiration date of the token. The expiration date ensures that the token doesn't last forever and that it expires at the designated time. The Payload is the body of the token. This is what an encoded token looks like:
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
Decoding it returns this:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
The Signature
The signature is obtained by taking the encoded header + the encoded payload and signing them using the secret key. How it signs the token depends on the algorithm defined in the header. It uses a secret key if it's HS256 and both a private and public key if it's RS256.
alg(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret_key)
What Makes JWT Vulnerable?
JWTs become a security risk when they are improperly implemented, potentially resulting in broken authentication and access control (allowing attackers to perform unauthorized actions like accessing restricted resources) or even account takeovers. According to OWASP (Open Worldwibe Application Security Project), Broken Authentication, which includes JWT weaknesses, is ranked #2 in the OWASP API security Top 10 risks. This highlights how common and impactful these vulnerabilities are.
Many of these flaws are based on how the JWT signature is designed to verify. Even if the signature is correctly verified for it to be trusted, it relies heavily on how the server manages the secret key.
Weak JWT implementations allow attackers to:
Bypass authentication and access control mechanisms, gaining access to restricted resources.
Manipulate the alg field to change the signing algorithm.
Brute force weak secret keys. This mainly happens when HS256 is used with weak entropy.
Exploit JWTs that accept the none alg field, allowing unsigned tokens.
Tamper with JWTs if weak signing keys are used.
Use expired JWTs if validation is not enforced.
Common Vulnerabilities in JWT:
In this section, I’ll detail the vulnerabilities mentioned above, provide sample code showing how they occur and how they are exploited, and suggest recommended fixes. (These code examples come from vulnerable applications used only for educational purposes). If you want more practice, check out a vulnerable API project I worked on. Use it to improve your skills in finding vulnerabilities and help developers to identify issues that arise from writing insecure code.
Let's dive into the fun part!
No signature verification
A common mistake developers make is confusing the decode() function with the verify() function when working with JWTs. While these may seem similar, their purposes are very different.
decode()
: As the name suggests, this function simply decodes the token from its base64url-encoded format. It does not check if the token is valid.verify()
: This function validates the signature using the secret/public key. It is this function that ensures the token hasn't been tampered with.
When JWT relies only on the decode() function, it allows anyone to easily tamper with the payload section.
This is a vulnerable sample code:
const jwt = require('jsonwebtoken');
const authMiddleware = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authorization required' });
}
const token = authHeader.split(' ')[1];
// No verification of token signature
const decoded = jwt.decode(token); // This does NOT verify the signature!
if (!decoded) {
return res.status(401).json({ error: 'Invalid token' });
}
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
};
module.exports = authMiddleware;
The code decodes the JWT without verifying its signature, making it vulnerable. The payload can be modified and still be accepted as valid.
Imagine this is the token for an API that lets you retrieve your user profile and the groups you belong to from an endpoint:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.zH6zmK7pXywgooJtKKv2lj2ny2RA1pnPtAFK5auTpCU
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "100",
"name": "John Doe",
"iat": 1516239022
}
You decode the token, modify the sub claim (which represents the user ID) from 100 to 101, encode it back using base64, and resend the token. The API responds with the profile and groups of user ID 101. The impact of the vulnerabilitiy is always high, often leading to serious security risks like unauthorized account takeovers and privileged actions.
Recommended Fix:
Always use the verify() function instead of just decode(). Remember, these functions serve different purposes.
Signature verification prevents tampering. If someone modifies the payload of a token, the original signature becomes invalid. Unless an attacker has access to the shared key (for HS256) or private key (for RS256), they won't be able to forge a valid token.
A secure version of the code earlier
const jwt = require('jsonwebtoken');
const authMiddleware = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
// Check if Authorization header is present and formatted correctly
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authorization required' });
}
const token = authHeader.split(' ')[1];
// Explicitly verify token signature and algorithm
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
});
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
} else if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
} else {
return res.status(500).json({ error: 'Internal server error' });
}
}
};
module.exports = authMiddleware;
- Ensure the algorithm is explicitly specified to avoid accepting any algorithm.
None Algorithm Attack
When the server blindly trusts user-controlled input from the alg
header, it lets an attacker change the alg
header, remove the signature, and send forged tokens in a valid format. Accepting the none
algorithm means the signature will be unsigned, and the server will accept the token as valid. This allows attackers to forge tokens and impersonate users without a valid signature. Without specifying the expected algorithm, like HS256, if the library allows it, the token is considered valid and can be used to access restricted resources and perform unauthorized actions.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
{
"alg": "none",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
When testing this vulnerability, you can try different case variations: none
, None
, noNe
, etc.
Recommended Fixe:
- Use libraries that do not accept none unless explicitly enabled.
Algorithm Confusion
Next up is the algorithm switching attack or algorithm confusion. This attack occurs when the JWT implementation trusts the alg field to determine how the signature should be verified. It becomes a problem when the server supports multiple algorithms and chooses how to verify based on the user-controlled input. Let’s consider a scenario where a JWT is legitimately signed using RS256; an attacker can modify the alg field to HS256 and use the public key (after they’ve obtained it from any public source) as the secret to generate a valid signature. And since the server sees the "alg": "HS256"
, it verifies the token using the public key as the shared secret, and this is passed as valid.
Here’s a vulnerable code to demonstrate this:
try:
# Get the header without verification to determine the algorithm
header = jwt.get_unverified_header(token)
algorithm = header.get('alg', 'HS256')
# Decode the token based on the algorithm
if algorithm == 'RS256':
# Use public key for RS256
payload = jwt.decode(
token,
settings.JWT_PUBLIC_KEY,
options={'verify_signature': True}
)
else:
# Default to HS256 with secret key
payload = jwt.decode(
token,
settings.JWT_SECRET
)
This code attempts to decode a JWT by first reading the alg field from the token header without verifying the signature. Based on the alg value, it chooses how to verify the token:
If it's RS256, it uses the server's public key for verification
If it's HS256, it uses a shared secret (shared meaning that it’s used for both signing and verification)
Consider this scenario where your apis signs tokens using RS256 with a private key:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMDAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.
Op_-9l7Yr6wWinvnSfWkfbxGfpb45rqJuHz0Xvhbu9AlxD29iWYP1M9m6Ootd4MrD45luOeq0CdjJrYfFedSyTyhVlJY0U-2s6mYrkJQIut3Qq_zkBihNZlTa09XB9CdAcecr5BUMhxvaxIXIVK53PAhzrJNcc-BSgej7MvWaOOMnSVNj1REB9-HSMoEpvhtSOO8k_CKAjR-RhvWY8odtLJ3ZmvFbPoXLqnVVbQ2r8gkRC2A6ApPlT5A0jak19OpIgYF87Nfs9aeiNdRuTaXB54128G1Z3uL6p1OrJ3WLY9ctnx005zmNkrIWvPWuKQFutaXJVCHSx5m-v21E5S2bA
{
"alg": "RS256",
"typ": "JWT"
}
Now, we get to modify the header to this:
{
"alg": "HS256",
"typ": "JWT"
}
Then, use the server's public key (which is often publicly accessible) as the shared secret to generate a valid HS256-signed token with elevated privileges
{
"sub": "100",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
Because the server doesn't use a fixed algorithm and accepts what the token claims, it verifies the tampered token as valid and grants access to another user's profile.
Recommended Fixes:
Never allows algorithm to be chosen dynamically from the token.
Hardcode the expected algorithm in your server logic
Validate the algorithm from the header before decoding or verifying.
Cracking weak JWT secrets
Algorithms like HS256 (HMAC with SHA-256) rely on a shared key to sign and verify a token. If this secret is weak, exposed, or predictable, it leaves room for attackers to brute force them using popular tools like JWT_tool. When the secret key is exposed or cracked, attackers can then create their own valid tokens to impersonate other users.
In the example image, the token is taken from the vulnerable API project I mentioned previously, which uses a weak secret key for HS256 to demonstrate this issue. I used the JWT_tool to successfully crack the secret used to sign the token:
From there, an attacker could modify the payload, for example, changing their user_ID or roles and then resigning the token using the cracked secret. This could allow them to perform unauthorized actions, such as deleting other users. If secrets aren't stored securely and get exposed (via version control or misconfigured servers), they can be extracted and used in the same way.
Recommended Fixes:
Use long, random keys for HS256.
Store secrets securely, such as in environment variables or secret managers.
Rotate secrets periodically and invalidate old tokens when secrets change.
Conclusion
If you've read this far, thank you. I hope this article helped you understand how common JWT vulnerabilities are introduced and how to avoid them. Whether you're a developer or security engineer, I hope you found value here. Security is a shared responsibility, and even small mistakes like JWT handling can lead to serious consequences. The good news? They are preventable, especially now that you know what to look out for.
If you have any questions, feedback, or even suggestions for future topics, feel free to drop them in the comments. I'd love to hear from you!
Subscribe to my newsletter
Read articles from Abigail Johnson directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abigail Johnson
Abigail Johnson
I secure web, API, mobile, and containerized applications, as well as cloud platforms (AWS, Azure, GCP), by testing for vulnerabilities. I also conduct secure code reviews and integrate security into software development (DevSecOps).