JWT Algorithm Confusion: How the Wrong `alg` Forges Any User
JWT algorithm confusion lets attackers sign forged tokens with your public key. Real CVE examples, 10-min self-test, library defaults, and the exact fix.
By TheZyberSecurity
TL;DR — JWT algorithm confusion lets an attacker sign forged tokens with your public key. Pin the algorithm server-side; never let the token choose.
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. Ships in production more often than teams expect. Maps to OWASP API2:2023 Broken Authentication, CWE-347 Improper Verification of Cryptographic Signature, and appears as a root-cause pattern in our web application penetration testing engagements. The fix is one line of intent: pin the algorithm server-side and never let the token choose.
Why is this JWT bug still alive 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:
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.- 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.
Real-world CVEs that made this a household example#
- CVE-2015-9235 (
node-jsonwebtoken) — the original public disclosure of algorithm confusion, published by Auth0 in March 2015. Same class of bug is still being cataloged a decade later against newer library wrappers. - CVE-2018-0114 (Cisco
node-jose) — key confusion via embedded JWK, letting an attacker inject their own public key into the token itself. - CVE-2022-21449 (Java 15–18
ECDSAsignature verification) — Oracle-shipped JDK accepted(0,0)as a valid signature; a distinct bug class, but the same conceptual family of "verifier trusts something it should verify." - CVE-2024-51678 and later disclosures throughout 2024-2025 — variants continue to appear against wrapper libraries and identity gateways that don't pin the algorithm.
The pattern is durable. Every year a new library or a new configuration surface reintroduces the same primitive.
Why a CVE 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.
Similar reasoning applies to entire families of authentication vulnerabilities. If you have not read the OWASP LLM Top 10 for founders shipping AI and wired the same "trust nothing from the token or the prompt" discipline into your LLM tool authorizations, algorithm confusion is only one instance of the class you are still exposed to.
How do we fix JWT algorithm confusion and prove it stays 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
noneat the edge. Treat an unsigned token as a malformed request, not an auth failure to log-and-continue. RFC 7519 §6 states unsecured JWTs are allowed only if both parties explicitly opted in — treat "explicit opt-in" as a compile-time constant, not a runtime possibility. - 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. That's the deliverable standard on every finding across our web application penetration testing engagements — reproducible PoC + fix + regression test.
How do we test our own JWT verifier in 10 minutes?#
The self-test is short. Grab a valid JWT your app issued. In one terminal:
- Decode the token (base64url, no signature required — use
jwt.ioorpython -c "import base64, json; print(json.dumps(json.loads(base64.urlsafe_b64decode('...').decode())))") - Modify a claim you care about (e.g.,
role: admin) and re-encode - Re-sign the tampered token as HS256 using your app's public key as the HMAC secret — libraries:
pyjwt,node-jsonwebtoken, orjwt.iowith algorithm override - Send the forged token to any authenticated endpoint
- If the request succeeds → your verifier is vulnerable. If the endpoint rejects with
401or400→ your allow-list is pinned correctly.
Do this against staging, not production. The exercise takes under 10 minutes and gives you a binary answer.
Which JWT libraries got the defaults right — and which did not#
jsonwebtoken(Node) — since v9 (2022) requiresalgorithmsallow-list onverify. Older versions silently accepted any algorithm.PyJWT(Python) — since v2.0 (2020) requires the algorithm parameter. Older versions had permissive defaults.jose(Go, JS) — encourages explicit algorithm binding via.verify()options; still requires the caller to specify.java-jwt(auth0) — verification builder pattern forces explicit algorithm; hard to misuse.nimbus-jose-jwt(Java) — explicitJWSVerifierper algorithm; safe by construction.golang.org/x/crypto/x/oauth2— leaves algorithm handling to callers; wrapper code frequently misses the allow-list.- Custom wrappers — the biggest risk surface. Teams write
verifyToken(token, key)helpers that lose the algorithm parameter along the way. Every custom wrapper deserves an audit.
If your stack uses managed identity providers (Auth0, Okta, AWS Cognito, Azure AD), the IdP's issuing side is usually safe. The verifying side — your gateways, microservices, and background workers that check the IdP's tokens — is where confusion lives.
Where this maps#
- OWASP API Security Top 10 (2023) — API2:2023 Broken Authentication
- OWASP ASVS v5 — V3 Session Management (token verification), V6.2.2 Cryptographic key management
- OWASP Web Security Testing Guide (WSTG) — WSTG-ATHN Authentication Testing
- CWE-347 — Improper Verification of Cryptographic Signature
- CWE-345 — Insufficient Verification of Data Authenticity
- NIST SP 800-63B — Digital Identity Guidelines (authenticator assurance)
- RFC 7519 §6 — JWT specification on
nonealgorithm handling - RFC 8725 — JSON Web Token Best Current Practices
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.
How do I test for JWT algorithm confusion without breaking production? Run the 10-minute self-test above in a staging environment that mirrors production auth configuration. Never test forged tokens against production endpoints — even a failed attempt shows up in intrusion detection logs and can trip incident response. If staging doesn't mirror auth exactly, ask for a scoped test window on a dedicated environment.
Does the JWT kid (key ID) header create a similar risk?
Yes — a different class of the same family. A permissive verifier that fetches the key based on the kid field can be tricked into fetching an attacker-controlled key (via path traversal, SSRF, or SQL injection in the kid value). Sanitize kid values against an allow-list of known key identifiers, not against a filesystem or database lookup.
Which JWT libraries had this as a default in the past?
jsonwebtoken (Node) versions before v9 (2022), PyJWT before v2.0 (2020), and many custom in-house wrappers. If your codebase pins those older major versions or has a custom verify wrapper that hides the algorithms parameter, treat it as a red flag.
What frameworks got this right by default?
Auth0's java-jwt (Java), nimbus-jose-jwt (Java), and modern jose (Go, JS) enforce explicit algorithm binding by construction — verification does not compile without it. If you have a choice of libraries, prefer the ones where misuse is a type error, not a runtime bug.
Related service
Web Application Pen Testing
OWASP-plus testing for modern web + API stacks.
See how we test itRelated field notes
- AppSecAugust 15, 202640 min read
Web Application Penetration Testing: Complete Guide for Indian SaaS Founders (2026)
OWASP Top 10 + API Top 10 + ASVS v5, six-phase methodology, INR cost brackets, DPDP Act + HIPAA overlays — WAPT for Indian SaaS founders in 2026.
- AI SecurityAugust 15, 202642 min read
AI Red-Teaming: Complete Guide for AI-Native SaaS in 2026
AI red-teaming for AI-native Indian SaaS: attack surface, OWASP LLM Top 10 v2.0, MITRE ATLAS, ISO 42001 alignment, and how to scope an engagement.
- AI SecurityAugust 15, 202617 min read
Prompt Injection Defenses That Actually Work in 2026
Prompt-based guardrails do not stop prompt injection. What actually works: architectural defenses, tool-scope enforcement, output escape, detection layers.