JSON Web Tokens (JWTs) are widely used for authentication, authorization, and secure information exchange in modern web applications. They're often used in OAuth2 flows, stateless session handling, API access, and SSO implementations.
A JWT consists of three parts, separated by dots:
HEADER.PAYLOAD.SIGNATURE
HS256
).{"user": "user1", "admin": false}
).Each part is Base64URL-encoded (without padding) and concatenated with a dot:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyMSIsImFkbWluIjpmYWxzZX0.
X5cBA0klC0df_vxTqM-M1WOUbE8Qzj0Kh3w_N6Y7LkI
The JWT specification supports multiple algorithms, defined in the JWA (JSON Web Algorithms) specification:
HS256
, HS384
, HS512
RS256
(RSA based), ES256
(Elliptic Curve based), PS256
(RSA based with MGF1 padding), etc.When a token is issued, it’s signed by the issuer using the specified algorithm. The recipient must verify the signature before trusting the payload.
The code to sign a token signs the concatenation of header + "." + payload
based on the ALGORITHM
picked by the developer:
signature = ALGORITHM.Sign(header + "." + payload, key)
In the same way the verification is done on header + "." + payload
:
ALGORITHM.Verify(signature, header + "." + payload, key)
For the verification, there are multiple strategies developers can use to pick the ALGORITHM
, they can hardcode it (safer) or use the value coming from the JWT header (attacker-controlled, not as safe).
In modern architectures, a single web application can be composed of dozens of microservices. Even if they share a hostname, each service may:
This means every endpoint must be tested individually. Don’t assume that if the login or main API endpoint handles JWT securely, all others do too. A misconfigured service or third-party microservice might still be vulnerable.
Throughout this guide, we’ll cover the most common — and most dangerous — JWT implementation flaws, how they are exploited, and how to detect or defend against them. Each section links to PentesterLab exercises so you can practice the attacks in a hands-on environment.
One of the most common and dangerous implementation mistakes when using JWTs is failing to verify the signature. JWTs are not encrypted — their purpose is to provide integrity. This means the contents of the token can be viewed by anyone, but should not be trusted unless the signature has been verified.
Unfortunately, some applications skip this critical step. This often happens because developers use a library’s decode()
method instead of verify()
, or they temporarily disable signature verification during testing and forget to re-enable it.
If a JWT is not verified before use, an attacker can forge arbitrary claims. The steps are trivial:
{"user": "bob", "role": "user"}
to:
{"user": "admin", "role": "admin"}
header.payload.
(less likely to work)Authorization
header).If the server does not verify the signature, it will treat the forged claims as valid — and you’ll be authenticated as admin
.
Even experienced developers can make this mistake when trying to quickly inspect a token’s contents or during local testing.
This issue effectively renders JWT-based authentication useless if not properly handled.
decode()
or insecure JWT flows.You can try this exact attack in a hands-on lab:
👉 PentesterLab: JWT Without Signature Verification
The JWT specification allows tokens to specify the signing algorithm in their header using the "alg"
field. Early versions of many JWT libraries accepted None
or none
as a valid option, meaning the token was considered valid without a signature at all. This was mostly due to developers of the library following the JWT specification and implementing all the required algorithms.
This was originally intended for debugging or unsecured flows, but in practice, it opened a serious security hole when libraries did not explicitly disable or reject the none
algorithm.
To exploit a JWT implementation that allows "none"
:
{"alg": "HS256", "typ": "JWT"}
becomes:
{"alg": "none", "typ": "JWT"}
or
{"alg": "None", "typ": "JWT"}
{"user": "admin"}
base64url(header) + "." + base64url(payload) + "."
If the backend does not reject tokens with "alg": "none"
, it will accept this token as valid — and you’re now admin
without any cryptographic proof.
This issue effectively renders JWT-based authentication useless if not properly handled.
RS256
or HS256
."alg": "none"
at the parser level.Try this vulnerability in a hands-on lab:
👉 PentesterLab: JWT None Algorithm
When using HMAC-based algorithms like HS256
, the integrity of the JWT depends entirely on the secrecy and strength of the shared secret key. If the key is weak, guessable, or hardcoded, an attacker can brute-force it using a known JWT and use it to forge arbitrary tokens.
This vulnerability can be common in poorly secured APIs and test environments, and it often affects production systems due to careless key management.
The attacker needs just one valid token. With that, they can run an offline brute-force attack to recover the secret. Here's how:
header.payload.signature
.HMAC(secret, base64url(header) + "." + base64url(payload)) == signature
This entire attack can be performed offline, without generating noise or alerts on the target system.
Common weak secrets include:
"secret"
"123456"
"my-api"
)You can use a list of known JWT secrets like wallarm/jwt-secrets to increase your chance of recovering the secret.
Try this attack in a hands-on environment with a weak secret you can crack yourself:
👉 PentesterLab: JWT Trivial Secret
One of the most subtle, yet devastating, JWT vulnerabilities arises from algorithm confusion. This attack exploits the fact that the JWT header includes a user-controlled "alg"
parameter. If the server doesn’t enforce which algorithm is expected, an attacker can manipulate the header to cause the backend to verify the token using the wrong algorithm — often with catastrophic consequences.
The most common variant: swapping an RS256
(RSA) token to HS256
(HMAC), and then using the RSA public key (meant only for verification) as the HMAC secret.
This attack works because of how asymmetric (RSA) and symmetric (HMAC) algorithms function:
If the server trusts the "alg"
field from the token header and uses the public key as the HMAC secret, an attacker can:
{"alg": "RS256", "typ": "JWT"}
to:
{"alg": "HS256", "typ": "JWT"}
header.payload
using HMAC with the server’s RSA public key.If the server blindly uses HS256
and its public key as the HMAC secret, the forged token will validate — and the attacker can fully impersonate any user.
There are many ways to get access to the public key:
alg = RS256
only).Try this exact attack by forging a token using the public key as the HMAC secret:
👉 PentesterLab: JWT Algorithm Confusion and PentesterLab: JWT Algorithm Confusion with RSA Public Key Recovery
This variation of the algorithm confusion attack targets applications using ECDSA (Elliptic Curve Digital Signature Algorithm), for example ES256
. Just like the RSA-to-HMAC confusion, the core issue is that the application trusts the "alg"
field from the JWT header, and uses it to select the verification method and key type dynamically.
By changing the "alg"
field from ES256
(ECDSA) to HS256
(HMAC), an attacker can trick the server into verifying the token using an HMAC signature — and use the ECDSA public key as the HMAC secret.
Here’s how the attack works:
ES256
(ECDSA)."alg": "ES256"
to "alg": "HS256"
in the header."user": "admin"
).header.payload
using HMAC and the public ECDSA key as the secret.If the backend is vulnerable and uses the public key as a secret without validating the key type or the original algorithm, the forged HMAC will validate — and the attacker gains access with elevated privileges.
ECDSA is asymmetric: it uses a private key to sign and a public key to verify.
HMAC is symmetric: it uses the same secret key to sign and verify.
If a system allows switching from ECDSA to HMAC, and treats the public key as a secret (because it’s all it has access to), it creates an unsafe equivalence between asymmetric and symmetric cryptography — and the attacker takes full advantage of this confusion.
As with RSA, you can find the key in documentation, SDK or in mobile apps. Alternatively, you can programmatically recover two potential public keys from a signature. You can find more details and code to recover the ECDSA public keys in our blog: Algorithm Confusion Attacks against JWT using ECDSA.
alg = ES256
only).Try this attack in a lab that walks you through recovering the ECDSA public key and forging a JWT using HMAC:
👉 PentesterLab: JWT Algorithm Confusion with ECDSA Public Key Recovery
kid
Injection (Key ID Manipulation)The JWT header supports a field called "kid"
— short for Key ID. This field allows the token to indicate which key should be used to verify the signature. It is especially useful in systems with key rotation or multiple signing keys.
However, when applications dynamically fetch keys based on this field — especially from filesystems or databases — the kid
value becomes a dangerous injection point. If the application uses it insecurely (e.g., directly concatenating it into a file path or SQL query), attackers can manipulate it to point to keys they control or leak internal secrets.
In file-based key lookups, the application might do something like:
key_path = "/keys/" + kid
public_key = readFile(key_path)
An attacker can supply a JWT with:
"kid": "../../../../dev/null"
This results in:
/keys/../../../../dev/null → /dev/null
Since reading from /dev/null
will return an empty string, an attacker can forge a token and sign it with an empty string.
If the application loads keys from a database using an unsafe query:
SELECT key FROM keys WHERE kid = ''
The attacker can supply:
"kid": "zzzz' UNION SELECT '123' --"
This causes the application to fetch and use an attacker-supplied value (123
), which will successfully verify forged JWTs signed with the matching private key.
kid
strictly — never allow user-controlled paths or queries.kid
values with fixed file or key mappings.kid
values.Practice injecting a malicious kid
to control key selection and forge tokens:
👉 PentesterLab: JWT kid Injection and Directory Traversal
👉 PentesterLab: JWT kid Injection and RCE
👉 PentesterLab: JWT kid Injection and SQL Injection
JWTs can optionally include a JWK (JSON Web Key) directly inside the token header using the jwk
parameter. This is intended to allow token issuers to specify the public key that should be used to verify the token — particularly useful in distributed systems or rotating key setups.
However, if the server accepts any public key supplied in the token without proper validation (such as checking the issuer, key origin, or intended usage), an attacker can embed their own public key into the header and generate tokens that validate against it.
This vulnerability was publicly disclosed as CVE-2018-0114 and affected the popular PyJWT
library. It allowed attackers to bypass authentication by embedding their key and signing tokens with the matching private key.
To exploit this vulnerability, the attacker:
"user": "admin"
).jwk
field:
"jwk": {
"kty": "RSA",
"e": "AQAB",
"n": "..."
}
If the application naively uses the JWK from the token header, the attacker’s key is used to verify the token — making the forged token appear legitimate.
"use": "sig"
and not "enc"
)jwk
header parsing unless explicitly needed.Try forging a JWT using your own key and bypass verification using the embedded jwk
:
JWT supports additional headers like jku
(JWK Set URL) and x5u
(X.509 certificate URL) that point to external URLs where public keys can be retrieved. These fields are designed to help recipients dynamically fetch verification keys, especially in distributed or federated systems.
However, if the application does not strictly control the source of these URLs, it opens the door for Server-Side Request Forgery and using an attacker-controlled key. An attacker can host their own key set or certificate and sign tokens with their private key, then instruct the server (via jku
or x5u
) to download and trust that key.
To exploit this behavior, an attacker will:
jku
)x5u
)"alg": "RS256"
"jku": "https://attacker.com/jwks.json"
or "x5u": "https://attacker.com/cert.pem"
If the server accepts the remote key without validation, it will trust the token — because it successfully verifies with the attacker’s hosted key.
This attack can also be exploited by leveraging a file upload, header injection or open redirect
jku
or x5u
URLs.kid
values.Practice forging a token that the server will trust based on the jku
or x5u
field:
👉 PentesterLab: JWT JKU attacks
👉 PentesterLab: JWT JKU and File Upload
👉 PentesterLab: JWT JKU and Open Redirect
👉 PentesterLab: JWT JKU and Header Injection
In 2022, a critical vulnerability was discovered in the Java JDK’s ECDSA signature verification implementation. This bug, now known as the “Psychic Signature” vulnerability — allowed attackers to bypass digital signature verification entirely by submitting an invalid signature where both values (s
and r
) are set to zero.
Tracked as CVE-2022-21449, this bug impacted applications that used Java’s java.security.Signature
class to verify ECDSA-signed JWTs, especially when using algorithms like ES256
.
The core of the vulnerability is that the Java implementation incorrectly accepted the signature with r=0
and s=0
as valid, even though these values should never occur in legitimate ECDSA signatures.
To exploit the issue:
"alg": "ES256"
and a forged payload (e.g., "user": "admin"
).MAYCAQACAQA
)If the backend uses a vulnerable version of Java and ECDSA verification, it will accept the forged token as valid — bypassing all authentication and allowing privilege escalation.
r
and s
.r = 0
and s = 0
.Practice crafting a forged JWT using a zeroed signature to bypass verification:
👉 PentesterLab: JWT Psychic Signature aka CVE-2022-21449
JWTs are powerful tools for stateless authentication, but they come with a complex and subtle attack surface. As you've seen throughout this guide, the most devastating JWT vulnerabilities often stem from small misconfigurations, incorrect assumptions, or over-trusting user-controlled data.
And the danger is compounded in modern architectures: a single application might use JWTs in dozens of different places — APIs, microservices, SSO layers, mobile backends — all with potentially different libraries, configs, and logic.
If you're auditing or pentesting an app:
If you're a developer or security engineer:
alg
, kid
, jku
, x5u
, and jwk
)It’s one thing to understand an attack in theory — but another to pull it off under real-world constraints. That’s why each section of this guide links to a PentesterLab exercise where you can practice the attack in a safe, realistic environment based on real-world CVEs and misconfigurations.
👉 Start practicing now on PentesterLab and level up your JWT exploitation skills — for real.
PentesterLab teaches web security and vulnerability discovery through hands-on exercises — based on real bugs, real CVEs, and real-world applications. It’s used by red teams, appsec teams, and security researchers around the world to build deep, practical skills.
Whether you’re learning JWTs, diving into SAML, or reviewing code for subtle logic flaws — PentesterLab is where you turn knowledge into skill.