One of the most damaging JWT vulnerability classes comes from trusting the alg header. Here's exactly how algorithm confusion attacks work and how to stop them.
The root cause
JWT libraries verify a token by reading the algorithm from the token's own header and using it to decide how to check the signature. If a server's verification code trusts that value unconditionally, an attacker who controls the token content can also control how it gets verified — which is a textbook example of letting untrusted input drive a security decision.
The RS256-to-HS256 attack
This is the classic case. Say a server issues RS256 tokens: signed with a private key, verified with a public key that's often not even secret — it may be published in a JWKS endpoint or embedded in client code. An attacker who has the public key can:
1. Take any legitimate RS256 token (or craft a new payload).
2. Change the header's alg field from RS256 to HS256.
3. Sign the token using HMAC-SHA256 with the RSA public key as the secret.
4. Send it to a server whose verification logic reads alg from the token and, seeing HS256, calls the HMAC verification path using "the configured key" — which, because the developer wired things up to accept either algorithm dynamically, resolves to that same public key value.
Since HMAC verification only checks "was this signed with a value matching this key," and the attacker now knows that value (it was public), the forged signature checks out. The server accepts a token the attacker fabricated.
The `alg: none` attack
An older but still-seen variant: some early JWT libraries honored `alg: none` as a legitimate "unsigned" token if the server's code path allowed it, meaning an attacker could simply strip the signature entirely, set `alg` to `none`, and submit an unsigned token with any claims they like. Modern libraries reject this by default, but it's worth confirming in any older codebase or hand-rolled verifier.
Why this keeps happening
It's rarely a library bug — most well-maintained JWT libraries default to safe behavior. The vulnerability is almost always in **application code** that either passes an algorithm list dynamically based on the incoming token, uses an older library version with unsafe defaults, or writes a custom decode/verify routine "by hand" without fully understanding the spec.
How to actually prevent it
- Configure your verifier with an **explicit, fixed algorithm or short allow-list**, e.g. `{ algorithms: ["RS256"] }`. Never derive it from the token.
- Never use the same key material for both signing and verifying under different algorithms — if you use RS256, don't also configure the same server to accept HS256 tokens signed with related key material.
- Keep JWT libraries up to date — most of the historical CVEs in this space were fixed by changing unsafe defaults.
- If you publish a public key (via JWKS or otherwise), assume it is fully public and never usable as an HMAC secret anywhere in your system.
A quick self-audit
Look at your verification call site. If you can answer "yes" to any of these, you likely have exposure:
- Does your code read `alg` from the incoming token before deciding how to verify it?
- Do you pass an empty or wildcard algorithms list to your JWT library?
- Do you use the same value as both an HMAC secret and something published publicly (a JWKS key, a client-side constant)?
Checklist
- Pin verification to an explicit algorithm allow-list set by your server code, never the token.
- Reject `alg: none` unconditionally.
- Never let a public key double as an HMAC secret anywhere in your system.
- Keep JWT libraries current and review verification code during any auth refactor.
- Add a regression test that submits a forged HS256-signed token to an RS256-only endpoint and confirms rejection.