Cybersecurity / Networking

JWT Algorithm Confusion Vulnerability (HS256 vs RS256), Basic Explanation

· by DebuggedIt

Quick answer

The JWT algorithm confusion vulnerability exploits a library or application that trusts the algorithm specified inside the token itself, rather than enforcing...

The JWT algorithm confusion vulnerability exploits a library or application that trusts the algorithm specified inside the token itself, rather than enforcing a specific, expected algorithm — an attacker who knows your public key (which, for RS256, is intentionally public) can craft a token signed with that public key treated as an HS256 secret, and a vulnerable verifier accepts it as genuinely valid.

The Problem

JWTs support multiple signing algorithms, commonly HS256 (a symmetric algorithm, using one shared secret for both signing and verification) and RS256 (an asymmetric algorithm, using a private key to sign and a separate, safely public key to verify). A JWT's header includes an alg field declaring which algorithm was used — and if the verification code blindly trusts this field to decide how to verify the signature, rather than enforcing a specific expected algorithm, an attacker can exploit this flexibility.

Why It's Exploitable

Consider a system designed to use RS256: the server signs tokens with a private key, and verification uses the corresponding public key, which — being public by design — might be published in a JWKS endpoint, embedded in client-side code, or otherwise reasonably accessible to anyone, including a potential attacker. If the verification code is vulnerable to algorithm confusion, an attacker can:

  • Craft a malicious JWT with its header's alg field changed from RS256 to HS256
  • Sign this malicious token using HS256, but using the RS256 public key as the HS256 secret
  • If the verification code naively reads the alg field from the token and uses HS256 verification logic with the same public key value (since it doesn't distinguish between "public key for RS256" and "secret for HS256" — they're just both strings from the verifier's naive point of view), the signature check incorrectly succeeds, since the attacker legitimately knows the "secret" (which was never actually meant to be secret, since it's the public key)

This allows an attacker to forge arbitrary, apparently-valid tokens for any claims they choose, despite never having access to the genuine private signing key at all — a complete authentication bypass, entirely enabled by the verification code's failure to pin down and enforce the expected algorithm ahead of time.

The Fix

Always explicitly specify and enforce the expected algorithm(s) during verification, never deriving it from the token's own header:

// Vulnerable pattern - trusts the algorithm from the token itself
jwt.verify(token, publicKey);

// Correct - explicitly restricts to the expected algorithm
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

With the algorithm explicitly pinned to RS256, a maliciously crafted token declaring HS256 in its header is rejected outright, regardless of any signature manipulation, since the verification library refuses to even attempt HS256-style verification when explicitly restricted to RS256 only.

Most modern, well-maintained JWT libraries have addressed this vulnerability class by default in recent versions, requiring or strongly encouraging explicit algorithm specification — but confirm your specific library and version's actual current behavior, and don't assume older code (or code copied from an outdated tutorial or Stack Overflow answer) follows this safer, explicit pattern:

# Audit your codebase for every jwt.verify() (or equivalent) call,
# confirming each one explicitly specifies allowed algorithms
grep -rn "verify(" --include="*.js" | grep -i jwt

Never derive verification behavior purely from the token's own alg header field in custom-written verification code, if you're implementing any part of JWT handling yourself rather than relying entirely on a well-vetted library — the entire vulnerability stems from trusting attacker-controllable input (the token, and therefore its header) to determine how that same token should be verified.

If your system genuinely needs to support multiple algorithms (a legitimate but less common scenario, such as during a migration from one algorithm to another), explicitly list every genuinely acceptable algorithm rather than accepting anything the token happens to declare:

jwt.verify(token, keyForAlgorithm, { algorithms: ['RS256', 'RS384'] });

Critically, never include both a symmetric algorithm (HS256) and an asymmetric one (RS256) in the same allowed list when using the same key material for both — this specific combination is exactly the scenario that enables the algorithm confusion attack, since it's the mixing of symmetric and asymmetric handling around the same key value that creates the exploitable ambiguity in the first place.

Still Confused?

The core principle worth internalizing: never let a piece of untrusted, attacker-controllable input (like a JWT's own header) dictate how that same input should be validated. The verifier — your server-side code — should always be the one deciding, based on its own fixed, pre-configured expectations, exactly what algorithm and what key should be used for a given verification context, entirely independent of anything the token itself claims about how it was supposedly signed.