JWT Algorithm Confusion Attacks in Production APIs

Attackers can choose how your API verifies tokens by controlling unverified header fields.

· 9 min read · Updated
Cover illustration for “JWT Algorithm Confusion Attacks in Production APIs”
Authentication Flaws · September 16, 2026 · 9 min read · 2,056 words

If you configure a server to trust RSA signatures, it ends up trusting whatever the token says to trust. That's the inversion sitting at the center of every algorithm confusion attack: the token, which is supposed to be the thing getting checked, ends up choosing the method used to check it. Production APIs read the alg field out of the token header before anything about that token has been verified, and that single sequencing choice hands the attacker control over which verification function runs.

Algorithm confusion attacks trace to three mechanically distinct root causes, each requiring a separate fix. The first is header-driven algorithm selection: the server reads alg from an unverified token header and dispatches to the corresponding verification function, letting the attacker choose between RS256 and HS256 or substitute alg:none entirely; the fix is to pin the expected algorithm in server-side configuration and reject any token whose header alg field does not match, before any cryptographic operation runs. The second is unsanitized key-selection metadata: the kid parameter feeds into file-path lookups or database queries without validation, enabling path traversal and SQL injection that replace the verification key with an attacker-controlled value; the fix is parameterized queries and allowlist-based input validation on every field that touches key retrieval. The third is remote key-source trust: the jku and jwk header fields can redirect or replace the public key used for verification, so a server that fetches whatever URL the token names, or that reads an embedded key from the header, is using attacker-supplied trust anchors; the fix is a strict server-side allowlist of permitted key-source domains, with jwk embedding disabled entirely, because an allowlist alone does not close SSRF routes through open redirects on permitted domains.

The sequence looks simple on paper. Decode the header, read alg, instantiate the matching verifier, then validate the signature. The attacker's input crosses the authentication boundary at step one, before the server has established any trust. By the time the signature check happens, the server has already let the token pick its own exam.

The flaw is separate from anything that happens inside RSA or HMAC, and neither algorithm is broken. RSA still does what RSA does, and HMAC still does what HMAC does. The flaw sits in the architecture surrounding them: whoever controls the header controls which cryptographic function runs, not whether that function works correctly once it runs. Authentication is supposed to work the other way around, with the server's own configuration deciding the algorithm and the token carrying nothing but data. Algorithm confusion attacks exist because that arrangement got flipped.

RFC 7519 and algorithm selection on the wrong side of verification

The inversion traces back to the specification itself. RFC 7519 made alg a token-controlled header field instead of a server-side configuration parameter, and that decision gave any token presenter, legitimate or not, the authority to nominate their own verification rules.

The alg:none case shows how deliberate this was. RFC 7519 section 6 defined unsigned JWTs as a valid token type, so libraries that accepted alg:none were implementing the specification correctly rather than deviating from it. Tim McLean's disclosure in March 2015 laid out the consequences: versions of auth0/jsonwebtoken before 4.2.2 accepted alg:none both through RFC compliance and through a permissive implementation default, and the identical flaw turned up at the same time in node-jsonwebtoken, pyjwt, namshi/jose, php-jwt, and jsjwt.

The fix took five years to arrive in writing. RFC 8725, published in February 2020, advised that libraries SHOULD NOT generate or consume alg:none JWTs unless a caller explicitly requests it. That's a best-practice addendum layered on top of the original spec rather than a correction to RFC 7519 itself.

There's a fair objection to raise here. RFC 7519 section 11, its Security Considerations section, did assign algorithm verification responsibility to implementers. The spec documented the risk and delegated the decision to whoever built the library. The flaw lives in the default behavior the spec encouraged, not in some oversight by its authors. That distinction matters, but it doesn't change the outcome. An insecure path that requires active, independent effort to disable in every single implementation is a bad default at the specification level. The same mistake appearing across dozens of unrelated libraries over more than a decade is the evidence for that, not a counterargument to it.

The practical consequence runs straight into remediation. Patching one library's defaults doesn't touch the next one. Because the flaw originates in how the spec structured the token rather than in any one codebase, fixing it durably means pinning the algorithm at the application layer, on the server, independent of whatever the token claims about itself.

What are the three mechanically distinct root causes of JWT algorithm confusion attacks, and how do you fix each one?

Diagram: How a Token Picks Its Own Exam: The Verification Sequence. Visualizes: Illustrate the four-step order-of-operations flaw at the heart of algorithm confusion attacks.

The order-of-operations flaw takes four distinct forms, and each one routes token-controlled metadata into server verification logic through a different door.

The order-of-operations flaw manifests in four distinct attack variants, each exploiting a different way that token-controlled metadata reaches server verification logic, one of which is RS256 → HS256 confusion, in which a server configured for RS256 receives a token whose header has been changed to read "alg":"HS256", signed using the server's own RSA public key as if it were an HMAC secret. That public key is public by definition, so nothing about this requires stolen credentials or insider access. If a library trusts the header, it switches from RSA verification to HMAC verification, treats the public key as the HMAC secret, finds that the signature matches, and accepts forged claims.

The alg:none bypass needs even less. Take any valid JWT, decode the header, change "alg" to "none", modify the payload (escalating a role to admin, for instance), re-encode it, and strip off the signature segment entirely. Defenses that try to blocklist the literal string "none" miss variants like "NONE", "None", or "nOnE", so case normalization isn't optional, it's a requirement for any blocklist that wants to actually work.

kid parameter injection works differently. The kid, or Key ID, field tells the server which key to use for verification, and frameworks that build file paths or database queries directly from that field without sanitizing it expose CWE-22 and CWE-89 inside a JWT context. A path traversal pattern like kid: "../../../dev/null" causes the server to open that file, read back empty content, and use the empty string as the HMAC secret, at which point an attacker just sends an HS256 token signed with an empty string. A SQL injection pattern like kid: "x' UNION SELECT 'attacker-key'--" gets the server to execute the malicious query and hand back the attacker's own value as the verification secret. Neither of these is a JWT library bug. They're classic injection vulnerabilities that happen to occur in a JWT context, and the fix is the same fix that applies to injection anywhere else: parameterized queries and input allowlisting.

JKU and JWK header injection target the key-fetching step directly. The jku header tells the server where to go fetch public keys, so if an application fetches that URL without validating the domain, an attacker can host a custom JWK Set and nominate their own public key as the trust anchor. The exploit runs in three steps: generate an RSA key pair, host a JWKS file at a URL the attacker controls, and insert that URL into the token's jku header before signing with the matching private key. A related variant, JWK injection, skips the external hosting step and embeds the public key directly inside the header's jwk parameter; the server reads the embedded key, checks the signature against it, and confirms the signature is valid, because mathematically, it is. Domain allowlisting on jku closes the obvious door but not all of them: URL confusion bypasses, SSRF through open redirects hosted on an allowed domain, and HTTP parameter pollution all remain common routes around an allowlist.

A fifth pattern deserves a mention even though it isn't a cryptographic attack. Applications that call a library's decode() method instead of its verify() method extract claims without ever checking a signature. They'll accept any token handed to them, valid or not, with no attack technique required on the attacker's end.

Recent CVEs that confirm this is an active, recurring vulnerability pattern

The CVE record for algorithm confusion isn't a historical footnote. The same root cause keeps reproducing across independent codebases written in Go, Python, Java, TypeScript, C, and Swift, pointing to a systemic failure in how verification gets modeled rather than a string of isolated coding mistakes.

CVE-2026-22817 hit the Hono framework, affecting every version before 4.11.4, with a CVSS score of 8.2. Hono is one of the fastest-growing edge-runtime frameworks for Cloudflare Workers, Deno, and Bun, and this was RS256-to-HS256 confusion, the same pattern described above, with a public proof-of-concept exploit available.

CVE-2026-23993 struck HarbourJwt, part of the Harbour library. Its GetSignature() function returns an empty string for any algorithm value it doesn't recognize, so an attacker can set alg to literally anything, "zzz" or "banana" work fine, and the empty computed signature matches the empty token signature. Verification passes with zero cryptographic knowledge required on the attacker's part.

CVE-2026-29000 hit pac4j-jwt with a CVSS score of 9.1, a JWE variant where the inner token was an unsigned PlainJWT carrying alg:none. So attackers could forge admin tokens with nothing more than the library's own public /jwks endpoint.

PyJWT, the most widely deployed JWT library in the Python ecosystem, was hit by CVE-2026-48526, where attackers forged tokens through a gap in HMAC key validation for JWK-format keys. CVE-2025-30144 hit Spring Security JOSE at a CVSS of 8.8, combining JKU injection with SSRF to bypass signature checks. CVE-2025-27371 affected golang-jwt/jwt v4 at a CVSS of 8.5, enabling privilege escalation.

CVE-2022-21449, nicknamed "Psychic Signatures," hit Java versions 15 through 18, where ECDSA verification accepted signatures with both the r and s values set to zero, satisfying the verification equation trivially. That single flaw affected TLS, JWT, SAML, and WebAuthn on every Java 15–18 system, and was fixed in Java 17.0.3 and 18.0.1 in April 2022.

What makes the first quarter of 2026 notable is the clustering. Hono, HarbourJwt, pac4j-jwt, Keycloak, and jose-swift, the last one tracked as GHSA-88q6-jcjg-hvmw for a similar unknown-algorithm bypass, all appeared in the same window. Library authors across the ecosystem, working independently of one another, are still building verification logic that trusts metadata the token itself supplies, which shows five independent teams building correctly against a specification that invites this exact failure, the whole argument for treating the spec, not any single codebase, as the place the fix needs to happen.

Microservice architectures and the blast radius of algorithm confusion

A monolith with this flaw has one door to lock. A microservice architecture has dozens, and they often share the same broken key.

Algorithm confusion in a distributed system doesn't fail at a single endpoint. It fails at the trust model connecting every service that relies on the same verification logic, so any one service running a generic verifier becomes an entry point for forged tokens into the whole system. If a gateway validates a token correctly at the edge, that doesn't protect a downstream service when the service trusts the gateway's word instead of checking the token itself.

Multiple token types share the same verification infrastructure: browser session tokens, machine-to-machine API tokens, password reset tokens, email verification tokens, signed webhooks, and internal service tokens may all be JWTs, each with its own policy requirements, often running through a single shared library. That sharing is efficient and it's also where the danger concentrates. A password reset token and an admin session token have no business being interchangeable, but if both pass through the same generic verifier with the same permissive defaults, the distinction exists only on paper.

Risk is highest when code reuses a generic verifier across token types, or when an API gateway validates one token and a downstream service trusts it without re-verifying. You see both patterns a lot in microservice deployments built for speed, where shared libraries and shared trust assumptions cut development time. Neither pattern isolates the blast radius of a single algorithm confusion bug. In a system built this way, one verifier that trusts the wrong field in one token doesn't compromise one account. It compromises the assumption every other service in the system was built on.

Sources

  1. JWT algorithm confusion attacks: How they work and how to prevent them — WorkOS
  2. JWT Algorithm Confusion Attack (RS256 vs HS256) | Security Vulnerability Database | Sourcery
  3. JWT Algorithm Confusion Attack: Two Active CVEs in 2026 | PinusX Blog
  4. jfrog.com

More in Authentication Flaws