In this article
April 8, 2026
April 8, 2026

JWT algorithm confusion attacks: How they work and how to prevent them

A complete breakdown of one of the most dangerous JWT vulnerabilities, from the cryptographic mechanics to the defensive code patterns that stop it.

Algorithm confusion is arguably the most impactful class of vulnerability in the history of JSON Web Tokens. It exploits a fundamental tension in the JWT specification: tokens carry metadata about how they should be verified, and if a server trusts that metadata, an attacker can subvert the entire authentication system without ever obtaining a private key or secret.

Despite being well documented since 2015, algorithm confusion vulnerabilities continue to surface in production systems. The root cause is subtle enough that developers who understand JWT mechanics can still get it wrong.

The setup: How JWT verification is supposed to work

To understand the attack, you need a clear picture of what happens during normal RS256 token verification.

The authorization server holds an RSA key pair. It signs tokens with the private key. Downstream services verify tokens using the public key, which is typically fetched from a JWKS (JSON Web Key Set) endpoint.

When a service receives a token, the standard verification flow looks like this:

  1. Decode the Base64url header to read the alg field.
  2. Select the appropriate verification function based on the algorithm.
  3. Retrieve the correct key.
  4. Run the cryptographic verification.

Step 2 is where the vulnerability lives. If the server uses the token's own alg claim to decide how to verify the signature, the attacker controls the verification path.

The classic attack: RS256 to HS256

Suppose a server is configured with an RSA key pair for RS256. The public key is available, either published at a JWKS endpoint, embedded in a TLS certificate, or simply discoverable through other means.

The attacker crafts a token with the following manipulations:

  1. Set the header's alg field to HS256 instead of RS256.
  2. Construct whatever payload they want (elevated privileges, a different user ID, an extended expiration).
  3. Sign the token using HMAC-SHA256, with the server's RSA public key as the HMAC secret.

When this token reaches the server, the vulnerable verification logic reads "alg": "HS256" from the header and branches into the HMAC verification path. It needs a symmetric key for HMAC, and in many implementations, the same key variable or configuration value is passed to both RSA and HMAC verification functions. That variable contains the RSA public key.

So the server computes HMAC-SHA256 over the header and payload using the RSA public key as the secret. The attacker computed the exact same HMAC with the exact same key. The signatures match. The forged token passes verification.

The attacker has just created a validly signed token for any identity they choose, using only publicly available information.

Why the math works out

There is nothing special about an RSA public key from HMAC's perspective. HMAC-SHA256 accepts any byte string as a key. It does not know or care that the bytes happen to represent an RSA public key. It simply uses those bytes as input to the keyed hash function.

The attack works because the public key is a known value, and HMAC is symmetric. If both the attacker and the server use the same key and the same message, they get the same output. The entire security model of HS256 depends on the key being secret, and an RSA public key is, by definition, not secret.

Beyond the classic: related confusion vectors

The alg: none attack

The JWT specification defines a none algorithm for unsecured tokens. If a server's verification logic respects this, an attacker can strip the signature entirely:

  
{"alg": "none", "typ": "JWT"}
  

The token becomes header + payload + empty signature (a trailing period with nothing after it). A naive implementation that dispatches on the alg field may skip verification altogether and accept the token as valid.

This is less common in modern libraries because most now reject none by default, but it still appears in custom JWT implementations and misconfigured systems.

JWKS injection via jku and x5u

The JWT header supports optional parameters that tell the verifier where to find the signing key:

The jku (JWK Set URL) parameter points to a URL hosting a JWKS.The x5u (X.509 URL) parameter points to a certificate chain.

If a server fetches keys from URLs specified in the token header without strict validation, an attacker can host their own JWKS at a URL they control, point the jku to it, sign the token with their own private key, and include the corresponding public key in their hosted JWKS.

The server fetches the attacker's JWKS, finds a key matching the token's kid, verifies the signature against the attacker's public key, and accepts the forged token.

Defenses include maintaining an allowlist of trusted jku URLs, ignoring these header parameters entirely, or only resolving keys from a locally configured JWKS endpoint.

Cross-algorithm confusion in asymmetric families

Less commonly discussed is confusion between different asymmetric algorithms. For example, a system expecting RS256 might accept a token signed with PS256 (RSA-PSS) if both share the same key and the verification logic switches based on the header. While this does not always produce a direct exploit the way the HS256 swap does, it can lead to downgrade attacks or bypass signature verification quirks in specific libraries.

Real-world vulnerabilities and CVEs

Algorithm confusion is not a theoretical concern. It has been found and exploited in widely used libraries across every major ecosystem.

The Node.js jsonwebtoken library (one of the most downloaded JWT packages on npm) was vulnerable prior to version 4.2.2. The verify function accepted the algorithm from the token header by default, making the RS256-to-HS256 attack trivial. The fix introduced an algorithms option that must be explicitly provided.

Python's PyJWT had similar behavior in versions before 1.5.0. The decode function would use the header's algorithm unless an algorithms parameter was passed.

The ruby-jwt gem, the php-jwt library, and several Java JWT implementations all had variants of the same issue at different points in their histories.

The pattern across all of these was the same: the library API made it easy to verify tokens without specifying the expected algorithm, and the default behavior was to trust the token's header.

Defensive patterns

Pin the algorithm explicitly

This is the single most important defense. Your verification code must specify which algorithm it expects, and the library must reject tokens that use a different algorithm.

  
// Node.js (jsonwebtoken)
const payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
  

Never allow the list of acceptable algorithms to include both symmetric and asymmetric types for the same verification key. If you are using RS256, the algorithms list should contain RS256 and nothing else.

Enforce key type and algorithm agreement

A defense-in-depth approach checks that the key type matches the algorithm. If the algorithm is HS256, the key should be an opaque byte sequence, not an RSA key object. If the algorithm is RS256, the key should be an RSA public key, not a raw byte string.

Some libraries now perform this check internally. If yours does not, add it explicitly before calling the verification function.

  
from cryptography.hazmat.primitives.asymmetric import rsa

def verify_token(token, key):
    header = jwt.get_unverified_header(token)
    if header["alg"] == "RS256":
        if not isinstance(key, rsa.RSAPublicKey):
            raise ValueError("Key type mismatch for RS256")
    return jwt.decode(token, key, algorithms=["RS256"])
  

Ignore embedded key references

Do not fetch keys from URLs specified in the token header (jku, x5u) unless you validate them against a strict allowlist. The safest approach is to ignore these parameters entirely and resolve keys from your own configuration or a trusted JWKS endpoint that you control.

Similarly, treat the kid (Key ID) parameter with care. Use it to index into your own key store, but validate that it matches an expected format and exists in your key set. Do not let it be used as a file path, database query, or any other input that could lead to injection.

Use a well-maintained library and keep it updated

The ecosystem of JWT libraries has matured significantly since the first wave of algorithm confusion disclosures. Modern versions of major libraries default to safe behavior or require explicit algorithm specification. But "modern version" is the operative phrase. Audit your dependency tree. An outdated JWT library is one of the easiest vectors for this class of attack.

Validate the full header

Beyond the algorithm, validate that the JWT header contains only expected fields. Reject tokens with unexpected parameters like jku, x5u, jwk (an embedded public key), or crit (critical header parameters). A strict allowlist for header fields reduces the surface area for header manipulation attacks.

The deeper lesson: Cryptographic agility is a liability

Algorithm confusion is a specific instance of a general problem in protocol design. When a system supports multiple algorithms and lets the incoming message influence which one is used, the interaction between those algorithms creates attack surface that none of them has individually.

This is sometimes called the "cryptographic agility" tradeoff. Agility (supporting multiple algorithms for flexibility and future-proofing) is genuinely useful for managing transitions when an algorithm is deprecated. But it must be implemented so that the verifier, not the message, controls which algorithm is acceptable.

The JWT specification inherited this tension by embedding the algorithm in the token header. Later specifications like COSE (used in WebAuthn and CBOR-based protocols) adopted approaches where the algorithm is negotiated out of band or fixed by the application, reducing the attack surface.

If you are designing a new system, consider whether you actually need algorithm agility at all. In many cases, picking one algorithm (RS256 or ES256) and hardcoding it into your verification logic is simpler, safer, and entirely sufficient.

Conclusion

Algorithm confusion attacks succeed because they exploit a gap between what a developer assumes ("my server uses RS256, so tokens are RSA-signed") and what the code actually does ("my server reads the algorithm from the token and verifies accordingly"). Closing that gap requires a small amount of explicit, defensive code, but the consequences of leaving it open are total authentication bypass.

Pin your algorithms. Enforce key type agreement. Ignore embedded key URLs. Audit your JWT library. These are not optional hardening steps. They are the minimum requirements for secure JWT verification.

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.