By AndyPublished
The JWT "none" Algorithm Vulnerability: History, Mechanics, and Defence
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.
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']oralgorithms: ['ES256']. The verifier rejects any token whosealgisn't on the list. - ·Reject
alg:noneimplicitly: 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
algfield on the JWK should match the verification algorithm. An RSA key markedalg: RS256cannot be re-purposed for HS256 verification. - ·Verify
kidagainst 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 wherejwt.verify()is needed, decode-only paths are sometimes mistakenly used in authorization flows. - ·Verifiers that read
token.header.algand dispatch to different verification functions based on it, almost always vulnerable to confusion attacks. - ·Missing or empty
algorithmsoption onverify()calls, modern libraries usually default to a safe value, but explicit is safer. - ·
alg === 'none'oralg !== ...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.