Most JWT vulnerabilities are not flaws in the token format. They are flaws in the few lines of code that verify it, and a token that is verified badly still looks like it works. The result is an API that accepts forged or replayed tokens while every happy-path test stays green.
Last updated: 2026-11-02. CVE details and standards status below were checked against the primary sources listed at the end on that date.
Key takeaways
- A signed JWT guarantees integrity, not secrecy. The signature only means something if your verifier pins the algorithm, the key, and the claims.
- Six mistakes cover most of what goes wrong:
alg: none, algorithm confusion, weak HMAC secrets, trusting keys the token points to, skippingiss/aud/typchecks, and unpatched crypto underneath. - Each one has shipped in real libraries or runtimes with a CVE. The standards warned about them in 2015 and 2020.
- The first fix is the same for most of them: an explicit algorithm allowlist on every verify call.
- Scanners find the obvious cases. Cross-service replay and token-type confusion need someone who understands your architecture.
What a JWT does and does not guarantee
A JSON Web Token carries claims as JSON, wrapped as a signed (JWS) or encrypted (JWE) object. According to RFC 7519, the format was published in May 2015.
Most API tokens are signed, not encrypted. A signed token is base64url encoded, so anyone who holds it can read the payload. As the OWASP JSON Web Token Cheat Sheet notes, keep sensitive data out of it.
What you get is integrity: the contents were not changed after issue. You only get that if the verifier is strict about which algorithm, which key, and which claims it accepts. RFC 8725 (the JWT Best Current Practices) exists because libraries left those choices to the caller. Every mistake below lives in that gap.
Mistake 1: letting the token choose alg: none
The JWT header names the algorithm used to sign the token. One legal value is none, an unsecured token with no signature at all. RFC 7518 (section 3.6) says implementations “MUST NOT accept Unsecured JWSs by default.”
If your verify call lets the token’s header decide, an attacker can strip the signature and your API may still accept it.
This shipped in practice. According to CVE-2022-23540, jsonwebtoken for Node, versions up to 8.5.1, could default to none when verify() was called without an algorithms option. It was fixed in 9.0.0. NVD scores it 7.6 and the GitHub CNA scores it 6.4.
Blocklists do not save you. The OWASP testing guide (WSTG-SESS-10) tells testers to try case variants of none against case-insensitive filters.
The standards are tightening further. The IETF working-group draft rfc8725bis (a draft, not yet an RFC) would add a MUST for applications to opt in explicitly to none.
How to check: find every verify and decode call in your codebase. Each one should pass an explicit list of allowed algorithms.
Mistake 2: algorithm confusion (RS256 key used as an HS256 secret)
Here the server expects an asymmetric algorithm such as RS256. The attacker sends a token that claims HS256, signed using the server’s public key as the HMAC secret. Public keys are, by design, public. A naive library sees “HS256”, takes the key you gave it, and the signature checks out.
This is the oldest repeat offender in the JWT world:
- CVE-2015-9235:
jsonwebtokenbefore 4.2.2. NVD rates it CVSS 3.0 9.8. - CVE-2022-29217: PyJWT, fixed in 2.4.0. The documented workaround is an explicit algorithm list. NVD rates it 7.5.
- CVE-2022-23541:
jsonwebtokenup to 8.5.1, a key-retrieval misconfiguration issue. NVD rates it 6.3. - CVE-2024-54150: cjwt, a C library, published December 2024. It is still happening.
RFC 8725 (section 3.1) says libraries MUST let callers specify allowed algorithms, and that each key should be used with exactly one algorithm. OWASP’s guidance is the same: do not mix public-key and MAC algorithms in one verifier.
How to check: confirm each key maps to exactly one algorithm in code, and that a token claiming a different one is rejected.
Mistake 3: a guessable HMAC secret
With HS256, anyone who captures one valid token can try guesses against it offline. There is no login endpoint to rate-limit and nothing to lock out. RFC 8725 (section 2.2) describes this directly. A weak secret turns into a forged-token factory.
The rules are clear:
- RFC 7518 (section 3.2): the key MUST be at least as long as the hash output, which is 256 bits for HS256.
- RFC 8725 (section 3.5): human-memorable passwords MUST NOT be used directly as HMAC keys.
- OWASP: at least 160 bits of entropy from a cryptographically secure random generator, and no reuse across issuers, audiences, or purposes.
The failure pattern we look for is mundane: a placeholder such as secret or changeme, a framework’s example value, or a .env.example that made it to production. We are describing a pattern, not quoting a statistic.
How to check: read your production signing key’s source. If a person typed it, replace it with a random value of at least 256 bits and use a different one per service.
Mistake 4: trusting keys the token points to
JWT headers can carry hints about which key to use: jwk (an embedded key), jku and x5u (URLs to fetch keys from), and kid (a key identifier). Every one of those is attacker-controlled input.
The clearest real case is CVE-2018-0114. Cisco’s node-jose before 0.11.0 trusted a public key embedded in the token header, so an attacker could re-sign tokens with their own key. NVD rates it 7.5.
The other three fail in different ways, according to the OWASP testing guide and RFC 8725 (section 3.10):
kidused in a file path or a database lookup opens the door to path traversal or injection.jkuandx5umake your server fetch a URL the attacker chose. That is forgery plus server-side request forgery.- The RFC’s advice is to sanitise
kidand allowlist anyjku/x5uhosts.
How to check: keys should come from your server config, or from a pinned key-set URL for an issuer you already trust. Never from the token itself.
Mistake 5: valid signature, wrong token
A perfect signature proves who issued the token. It does not prove the token was meant for this API, or that it is the kind of token you expect.
Replay across services
Say one identity-provider tenant issues tokens for several apps and APIs. If your API never checks the audience (aud) claim, a token minted for service A is accepted by service B. RFC 8725 (section 3.9) says that when multiple relying parties share an issuer, aud MUST be present and checked.
This is an integration mistake, not a vendor bug. It is easy to make with any identity provider.
Confusing token types
ID tokens, email-verification tokens, and refresh tokens are all JWTs too. If your API accepts any validly signed token, one of those can be presented as an access token. RFC 8725 (sections 3.11 and 3.12) recommends explicit typ values and mutually exclusive validation rules. RFC 9068 defines typ: at+jwt for OAuth access tokens, and resource servers should reject other types.
Library gotchas
Per the OWASP cheat sheet, PyJWT needs the issuer= and audience= arguments to actually compare values. The verify_iss/verify_aud options alone do not. Also pick keys only from the matched issuer’s key set, never a union across issuers.
How to check: take a valid token from one service and send it to another. Then try an ID or refresh token on a protected route. Both should fail.
Mistake 6: assuming the crypto underneath is correct
Sometimes your code is right and the runtime is wrong. CVE-2022-21449 (“psychic signatures”) let Java’s ECDSA implementation accept a signature with r = s = 0 for any message. ES256 JWTs verified by an affected JVM could be forged. NVD rates it 7.5 and lists Java SE 17.0.2 and 18 as affected.
The researcher who found it reports a wider range, Java 15 to 18, fixed in the April 2022 Critical Patch Update, and says he reported it to OpenJDK on 11 November 2021. That account is his own write-up. We cite NVD for the affected versions and treat the wider range as reported.
The lesson is practical. Patching the runtime and the JWT library is part of auth security. And your tests need negative cases: forged and malformed tokens must fail, not just valid ones succeed.
What a correct verify call looks like
Here is the defensive version in Python with PyJWT 2.4.0 or later. The key comes from config, the algorithm is pinned, and the claims are required.
import jwt # PyJWT >= 2.4.0
claims = jwt.decode(
token,
key=public_key, # from config / pinned JWKS for THIS issuer
algorithms=["RS256"], # explicit allowlist; never read from the token
audience="https://api.example.com", # this service only
issuer="https://auth.example.com", # exact match
options={"require": ["exp", "iss", "aud"]},
)
# Then: check header typ == "at+jwt" (if your IdP issues RFC 9068 tokens)
# and authorize the specific object being requested.
The Node equivalent on jsonwebtoken 9.0.0 or later is jwt.verify(token, key, { algorithms: ['RS256'], audience, issuer }).
Note the last comment. A valid token with a missing object-level check is the next bug in line. That is the classic IDOR pattern: the app knows who you are but never asks whether this object is yours.
Revocation, theft, and what “stateless” costs you
A JWT is valid until its exp time. Revoking one early means keeping a denylist keyed on jti plus issuer (not the raw token) or a status list. OWASP is clear that either one brings state back.
That is a trade-off, not a reason to avoid JWTs:
- Keep access tokens short-lived, and rotate refresh tokens.
- Sender-constrain high-value tokens. RFC 9449 (DPoP) or mutual TLS makes a stolen token useless without the client’s key.
- Use the current reference. RFC 9700 (January 2025) is the OAuth 2.0 security best practice for token handling.
- Consider not using JWTs at all for a single first-party web app. OWASP suggests an opaque server-side session can be simpler and safer there.
JWT security best practices, in priority order
This is the order we would fix things, cheapest and highest-impact first.
- Pin the algorithm on every verify call, one algorithm per key. Cost is trivial. It only breaks things if you were quietly accepting mixed algorithms.
- Upgrade.
jsonwebtoken9.0.0 or later, PyJWT 2.4.0 or later, and a patched JDK. - Prefer asymmetric signing when more than one service verifies tokens (RS256, ES256, or EdDSA). If you stay on HS256, use a random secret of at least 256 bits that is not shared across services. The trade-off: asymmetric means you operate key-set rotation and caching.
- Take keys from config or a pinned key set only. Ignore
jwk/x5cin headers, allowlistjku/x5u, and treatkidas untrusted input. - Require and check
exp,iss,aud, plustypwhere your provider sets it. - Shorten token lifetime, and add DPoP or mutual TLS for high-value APIs. The cost is client complexity and key management.
- Add negative tests to CI. Tokens with
none, the wrong algorithm, the wrong audience, an expired time, or the wrong type must all be rejected.
How long this has been known
| Date | Event |
|---|---|
| May 2015 | RFC 7518/7519 published; none must not be accepted by default |
| 2015 (NVD entry 2018-05-29) | jsonwebtoken algorithm confusion, CVE-2015-9235 |
| 2018-01-04 | node-jose embedded key, CVE-2018-0114 |
| Feb 2020 | RFC 8725, JWT Best Current Practices (BCP 225) |
| 2022-04-19 | Java “psychic signatures”, CVE-2022-21449 |
| 2022-05-24 | PyJWT algorithm confusion, CVE-2022-29217 |
| 2022-12-22 | jsonwebtoken 9.0.0 fixes, CVE-2022-23540 and CVE-2022-23541 |
| 2024-12-19 | cjwt algorithm confusion, CVE-2024-54150 |
| Jan 2025 | RFC 9700, OAuth 2.0 Security BCP |
| Aug 2026 | rfc8725bis-10, an IETF draft submitted to the IESG |
How we test JWT handling
Our API and authentication testing follows the same sequence as the OWASP testing guide: decode the token, tamper with the algorithm, try algorithm confusion, test the secret’s strength, inject into header fields, and replay claims across services.
In our opinion, automated scanners are good at the first few and weak at the last. Cross-service aud replay and token-type confusion depend on knowing which services share an issuer and which token types your architecture issues. That needs a human who has read your design.
If your agent or MCP backends authenticate with JWT bearer tokens, the same checks apply there.
FAQ
Is a JWT encrypted?
No. A signed JWT is only base64url encoded, so anyone who holds it can read the payload. The signature proves it was not altered. It does not hide anything. Keep secrets and personal data out of the claims, or use an encrypted token (JWE) when confidentiality matters.
What is the JWT “alg: none” attack?
The attacker sets the token’s algorithm to none and removes the signature. If the verifier lets the token choose its own algorithm, it may accept the forged token. RFC 7518 says implementations must not accept unsecured tokens by default. Always pass an explicit algorithm allowlist when verifying.
What is JWT algorithm confusion (RS256 vs HS256)?
The server expects an asymmetric algorithm like RS256, but the attacker sends a token marked HS256 and signed with the server’s public key as the HMAC secret. A naive library verifies it. The fix is to pin one algorithm per key, as RFC 8725 requires.
How long should a JWT HMAC secret be?
At least as long as the hash output: 256 bits for HS256, according to RFC 7518. OWASP asks for at least 160 bits of entropy from a secure random generator. Never use a memorable password, and do not reuse one secret across services or purposes.
Which claims must I validate in a JWT?
Check exp so expired tokens fail, iss for the expected issuer, and aud so the token was meant for your API. Where your provider sets it, also check typ so an ID or refresh token cannot pass as an access token. Make the library require them.
Can you revoke a JWT?
Not by itself. A JWT stays valid until exp. To revoke early you need a denylist keyed on jti and issuer, or a status list, which reintroduces server-side state. Most teams keep access tokens short-lived, rotate refresh tokens, and sender-constrain sensitive tokens with DPoP or mutual TLS.
Sources
- RFC 7519, JSON Web Token
- RFC 7518, JSON Web Algorithms
- RFC 8725, JWT Best Current Practices
- RFC 9068, JWT Profile for OAuth 2.0 Access Tokens
- RFC 9449, OAuth 2.0 DPoP
- RFC 9700, Best Current Practice for OAuth 2.0 Security
- IETF draft-ietf-oauth-rfc8725bis
- OWASP JSON Web Token Cheat Sheet
- OWASP WSTG-SESS-10, Testing JSON Web Tokens
- NVD: CVE-2015-9235, CVE-2018-0114, CVE-2022-21449, CVE-2022-23540, CVE-2022-23541, CVE-2022-29217, CVE-2024-54150
- Reported by the discoverer: Psychic Signatures in Java
Want us to check how your API actually verifies tokens? Book a scoping call and we will map an API and authentication pentest to your stack. See what we test on our services page. Not ready to talk? Start with a free external attack-surface snapshot, no access needed: nomorebreach.com.

