JWT Vulnerabilities: 6 Mistakes That Let Tokens Be Forged

JWT Vulnerabilities: 6 Mistakes That Let Tokens Be Forged

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, skipping iss/aud/typ checks, 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: jsonwebtoken before 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: jsonwebtoken up 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):

  • kid used in a file path or a database lookup opens the door to path traversal or injection.
  • jku and x5u make your server fetch a URL the attacker chose. That is forgery plus server-side request forgery.
  • The RFC’s advice is to sanitise kid and allowlist any jku/x5u hosts.

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.

  1. 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.
  2. Upgrade. jsonwebtoken 9.0.0 or later, PyJWT 2.4.0 or later, and a patched JDK.
  3. 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.
  4. Take keys from config or a pinned key set only. Ignore jwk/x5c in headers, allowlist jku/x5u, and treat kid as untrusted input.
  5. Require and check exp, iss, aud, plus typ where your provider sets it.
  6. Shorten token lifetime, and add DPoP or mutual TLS for high-value APIs. The cost is client complexity and key management.
  7. 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


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.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *