GoWin Tools
Tools
โ† JWT Decoder

JWT Decoder ยท 6 min read

The "alg: none" Vulnerability That Broke Half the Internet's JWT Libraries

In 2015, security researchers found that most JWT libraries would happily accept a token with the signature algorithm set to 'none'. The fix took years. Here is how it happened.

In March 2015, Tim McLean published a short blog post titled "Critical vulnerabilities in JSON Web Token libraries." It described two bugs that affected most major JWT implementations โ€” including official libraries from Auth0, Microsoft, and Google. One of those bugs has its own name now: alg: none. It's the textbook example of how a well-meaning specification feature became a catastrophic default.

The Spec Said You Could

RFC 7519 defines a JWT as three Base64-encoded segments โ€” header, payload, signature โ€” joined by dots. The header declares which signing algorithm was used in the alg field. Common values are HS256 (HMAC-SHA-256), RS256 (RSA-SHA-256), and ES256 (ECDSA-P256-SHA-256).

The spec also defined none as a valid algorithm. The intent was reasonable: there are situations where a JWT is being passed inside an already-secure channel and a signature is redundant. The spec said clients mustverify the algorithm matches what they expect โ€” but that's a contract that's easy to break.

The Bug

Most JWT libraries exposed an API roughly like this:

jwt.verify(token, secret)

The library would parse the token, read the alg field from the header, and dispatch to the appropriate verifier. If the header said HS256, it would verify the HMAC. If the header said none, it would skip verification entirely and return the payload as valid.

That meant an attacker could take any signed token, modify the payload, change the header to {"alg": "none"}, drop the signature segment, and the library would accept it as authentic.

// Attacker's forged token (no signature):
eyJhbGciOiJub25lIn0.eyJ1c2VyIjoiYWRtaW4ifQ.

// Library accepts it. Welcome, admin.

The Second Bug: Algorithm Confusion

McLean's post described a second, related issue. If a server expected RS256 tokens (signed with an RSA private key, verified with the public key), an attacker could change the header to claim HS256. The library, seeing HS256, would verify the token using the secret it was given โ€” which in this case was the public RSA key, treated as an HMAC secret.

Public keys are, by definition, public. So the attacker could forge any token they wanted, sign it with HMAC-SHA-256 using the published RSA public key as the "secret," and the server would accept it. This was arguably worse than alg:none because servers thought they were doing the right thing.

Why So Many Libraries Got It Wrong

The flaw is in the API design, not just the implementation. jwt.verify(token, secret)looks symmetric โ€” pass the token, pass the key, get back validity โ€” but the security model requires the verifier to know in advance which algorithm to expect. The token's own header is hostile data and cannot be trusted to choose the algorithm.

The fix in modern libraries is to require the caller to specify the expected algorithm explicitly:

jwt.verify(token, key, { algorithms: ['RS256'] })

If the token's header doesn't match the expected algorithm, verification fails before any signature check happens. This shut down both alg:none and the RS256/HS256 confusion attack.

The Long Tail

The disclosure was in March 2015. Patches landed in major libraries within weeks. But the bug kept showing up in the wild for years afterward โ€” in libraries that had been patched but were used incorrectly, in libraries that had never been audited, in homegrown JWT verifiers written by someone who'd read the spec once. CVEs for alg:none variants continued to appear through 2023.

The OWASP JWT Cheat Sheet now explicitly recommends rejecting alg: none at parse time, before any other logic runs. Most modern libraries will refuse to even parse such a token unless you opt in.

Lessons

  • Untrusted data should never select the verification routine. The token says what it is; the verifier decides what it expects. They're different concerns.
  • Defaults matter more than features. The none algorithm exists for a defensible reason but should never have been on by default.
  • API symmetry is a footgun. A function that signs and a function that verifies have different security models, even if they look similar.
  • Cryptographic agility cuts both ways. Letting the protocol negotiate the algorithm enables future migration but creates downgrade attacks if the negotiation isn't authenticated.

What to Do Today

If you're using JWTs, pin the algorithm explicitly when verifying. Reject none at the application layer even if your library claims to. Rotate keys. Use short token lifetimes. And consider whether you actually need JWTs at all โ€” for many session use cases, a server-side session ID is simpler, smaller, and immune to this entire class of bug.

References

  1. Jones, M., Bradley, J., & Sakimura, N. (2015). RFC 7519: JSON Web Token (JWT). Internet Engineering Task Force.
  2. McLean, T. (2015). Critical vulnerabilities in JSON Web Token libraries. Auth0 blog.
  3. Jones, M. (2015). RFC 7515: JSON Web Signature (JWS). Internet Engineering Task Force.
  4. OWASP Foundation. (2023). JSON Web Token Cheat Sheet. OWASP.
  5. Auth0. (2016). Critical vulnerabilities in JSON Web Token libraries โ€” disclosure timeline. Auth0 Engineering blog.