By AndyPublished

The JWT "none" Algorithm Vulnerability: History, Mechanics, and Defence

In March 2015, security researcher Tim McLean published a one-page write-up describing two JWT-library vulnerabilities. The first, the "none algorithm" attack, became the most-cited JWT security incident in history and reshaped how the entire ecosystem thinks about token verification. This guide reconstructs how it worked, why the JOSE specification permits alg:noneat all, the algorithm-confusion attacks that followed, and what every JWT verifier needs to do today.

What the Attack Actually Was

A JWT's header is a JSON object. One field, alg, declares which signing algorithm the token uses. RFC 7518 §3.6 specifies a permitted value: "none". When alg is "none", the JWT MUST have an empty signature segment, and the JOSE library treats the token as "unsecured".

That's by design, there are valid edge cases (transmission inside an already-encrypted channel, internal-only debugging, JWTs nested inside JWE). The problem was that many libraries' default verification flow looked like this:

// Vulnerable pseudo-code (2015-era)
function verify(token, key) {
  const { header, payload, signature } = split(token);
  const alg = header.alg;
  if (alg === 'none') return payload;       // ← the vulnerability
  if (alg === 'HS256') return verifyHmac(token, key);
  if (alg === 'RS256') return verifyRsa(token, key);
  // ...
}

An attacker would take a legitimate token, change the header to {"alg":"none","typ":"JWT"}, modify the payload to whatever they liked (say, {"sub":"admin","exp":9999999999}), and submit the token with an empty signature segment. The verifier readalg: "none" from the token itself, took the early-return path, and accepted the forged payload as authentic.

!
The root cause: the verifier let the token tell it how to verify the token. The defence is universal, the verifier must always pick the verification algorithm from a server-side allow-list, never from the token's own header.

Why Does the JOSE Spec Allow alg:none at All?

The spec design assumed that an "unsecured" JWT is a deliberately-constructed object, created knowingly, transmitted over an already-secure channel, never confused with a signed token. JWS (RFC 7515 §6) explicitly states that unsecured JWTs SHOULD NOT be used "for assertions of identity or authorization in production systems". The none algorithm exists so the JOSE ecosystem has a way to represent unsecured payloads at all, not because the spec wanted to enable a common production path.

The vulnerability was libraries treating the spec as "if alg is none, return the payload" without enforcing the surrounding context (that the caller had explicitly opted into accepting unsecured tokens). The spec is permissive; the implementations were trusting.

Which Libraries Were Affected

Tim McLean's original disclosure named two: node-jsonwebtoken (Node.js) and pyjwt (Python). Both shipped patches within days. But the broader industry audit that followed found similar vulnerabilities in:

  • ·jjwt (Java), multiple versions before 0.5.1
  • ·kjur/jsrsasign (JS), early versions
  • ·jose-php: patched 2015
  • ·Several Go libraries (pre-1.0 era), since fixed
  • ·Numerous in-house implementations that nobody publicly reported

By 2017 the major libraries had switched the default to "reject alg:noneunless explicitly opted-in", typically via a flag like algorithms: ['HS256']on verify().

The Algorithm-Confusion Variant

The second vulnerability in McLean's 2015 paper was algorithm confusion, and it survived thealg:none fix because it's a different bug. The setup: a service uses RS256 (asymmetric) and publishes its RSA public key via JWKS. An attacker takes the public key bytes, forges a token with {"alg":"HS256"}, and signs it using the RSA public key as the HMAC secret. A verifier that picks the verification function from the token header fetches the "key" (the public key), calls HMAC-SHA256 on it (because the header says HS256), and accepts.

The fix is the same as the none fix: never trust the token to tell you how to verify it. Pick the algorithm from configuration, then call the matching verifier, if the token'salg header doesn't match what you expected, reject before any cryptographic operation happens.

Modern Defences (What Every Verifier Must Do)

  • ·Algorithm allow-list, configured server-side: every JWT verification call passes an explicit list like algorithms: ['RS256'] or algorithms: ['ES256']. The verifier rejects any token whose alg isn't on the list.
  • ·Reject alg:none implicitly: even if it's somehow on the allow-list, modern libraries refuse to verify a "none"-signed token without an additional opt-in flag.
  • ·Key-type/algorithm binding via JWKS: when fetching a key via JWKS, the alg field on the JWK should match the verification algorithm. An RSA key marked alg: RS256 cannot be re-purposed for HS256 verification.
  • ·Verify kid against the published key set: make sure the key identifier from the token corresponds to a key your service legitimately issued.

Detecting the Pattern in Code Review

A few patterns to grep for in any JWT-handling codebase:

  • ·jwt.decode() used where jwt.verify() is needed, decode-only paths are sometimes mistakenly used in authorization flows.
  • ·Verifiers that read token.header.alg and dispatch to different verification functions based on it, almost always vulnerable to confusion attacks.
  • ·Missing or empty algorithms option on verify() calls, modern libraries usually default to a safe value, but explicit is safer.
  • ·alg === 'none' or alg !== ... checks that special-case the none algorithm, usually means the developer was thinking about it but possibly got the logic wrong.

Historical Impact

The alg:none disclosure marked the moment the JWT ecosystem stopped treating the standard as "just JSON + base64 + a signature" and started treating verification as a security contract. Every major JWT library now ships with safe defaults, every OWASP cheat sheet covers it, and most security-aware codebases have CI lint rules detecting the vulnerable pattern. It's the most important JWT security lesson of the past decade, and the pattern of "let the token tell you how to verify it" still resurfaces periodically in new code.

See It Yourself

Paste any JWT with {"alg":"none"} in its header into jwtdecode.app, the decoder will display the header and payload, and the verification tab will refuse to call it valid. The signature segment can be empty, junk, or whatever you like, without a verification algorithm and key, the tool will not pretend to authenticate it.

Summary

The JWT alg:none vulnerability was a 2015 implementation flaw, not a spec flaw, the spec permits the algorithm for narrow use cases, but libraries treated "none" as a valid verification result. The fix is universal: never let the token specify how to verify itself. The algorithm-confusion variant that followed (and still appears in new code today) is the same root cause. Server-side algorithm allow-listing closes both.

Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder