Skip to content
The //Zyber// Security
All posts
AppSecJuly 14, 20265 min read

JWT Algorithm Confusion: How the Wrong `alg` Forges Any User

Algorithm confusion lets an attacker sign valid JWTs with your public key. Why scanners miss it, how it works, and how to pin the algorithm and prove the fix.

By TheZyberSecurity

TL;DR — Algorithm confusion is when a JSON Web Token verifier can be tricked into accepting a token signed with the wrong algorithm. The classic case: a service that issues RS256 (asymmetric) tokens is handed an HS256 (symmetric) token, and it uses the public key as the HMAC secret. Because the public key is — by definition — public, an attacker can forge a valid token for any user, in any tenant. The fix is one line of intent: pin the algorithm server-side and never let the token choose.

Why this is still a live bug in 2026

JWTs carry their own algorithm in the header — {"alg":"RS256"}. For years, libraries trusted that field. Two failure modes fall straight out of that trust:

  1. alg: none. The token declares itself unsigned, and a permissive verifier accepts it with no signature at all. Set "alg":"none", drop the signature segment, rewrite the claims, and you are whoever you say you are.
  2. RS256 → HS256 confusion. The server expects an RS256 signature it checks with an RSA public key. The attacker re-signs the tampered token as HS256 — a symmetric HMAC — using that public key as the secret. A naive verify(token, publicKey) call feeds the same key into the HMAC path, and the signature matches.

The reason it survives modern stacks is mundane: the RSA public key is not a secret. It ships in your JWKS endpoint, your mobile app bundle, sometimes your front-end. So the "secret" the attacker needs is already in their hands.

Why a scanner won't catch it

A CVE scanner looks for known-vulnerable versions. Algorithm confusion is almost never a library CVE — it is how your code calls the library. jwt.verify(token, key) with no algorithms allow-list is valid, up-to-date, and exploitable. Nothing in a dependency graph flags it. This is exactly the class of finding that needs a human reading the auth path, not a version diff.

How to fix it — and prove it's fixed

  • Pin the algorithm. Pass an explicit allow-list on every verify: verify(token, key, { algorithms: ["RS256"] }). Reject anything else before the signature is even checked.
  • Separate key material by type. The key you use to verify RS256 should never be reachable by an HMAC code path. Different key objects, different functions.
  • Reject none at the edge. Treat an unsigned token as a malformed request, not an auth failure to log-and-continue.
  • Prove it with a test. A one-line negative test — "an HS256 token signed with our public key is rejected" — turns a subtle assumption into a regression gate.

A good report doesn't stop at "you're vulnerable." It hands you the forged token, the exact verify call at fault, and the passing test that closes it.

Where this maps

  • OWASP API Security Top 10 — API2:2023 Broken Authentication
  • OWASP ASVS — V3 Session Management (token verification)
  • CWE-347 — Improper Verification of Cryptographic Signature

FAQ

Is alg:none really still exploitable? On a correctly configured library, no. But defaults drift, wrappers get written, and one service in a fleet forgets the allow-list. We find it by testing the actual endpoints, not by trusting the framework's reputation.

Does rotating keys help? No. Rotation limits the blast radius of a leaked private key. Algorithm confusion never needs the private key — it abuses the public one — so rotation changes nothing here.

We use a managed identity provider. Are we safe? The IdP's own verification is usually fine. The risk moves to your services that validate the IdP's tokens — gateways, microservices, background workers. Those are the code paths we test.

AuthenticationJWTOWASPAppSec

Related service

Web Application Pen Testing

OWASP-plus testing for modern web + API stacks.

See how we test it