Safeguard
Vulnerability Analysis

JWT algorithm confusion / none-algorithm bypass explained

How attackers forge JWTs via RS256/HS256 key confusion and the alg:none bypass, with real CVEs, detection steps, and defenses.

James
Principal Security Architect
7 min read

JWT algorithm confusion is a signature-verification flaw that lets an attacker forge a valid-looking JSON Web Token by tricking a server into checking it with the wrong cryptographic algorithm. The bug lives in how JWT libraries handle the alg field inside the token header — a value the attacker fully controls before any signature is checked. Two variants dominate real-world exploitation: swapping an RS256 (asymmetric) token for HS256 (symmetric) and reusing the server's own public RSA key as the HMAC secret, and the older alg: none bypass, where a library skips signature verification entirely because the header claims no signature exists. Both were mapped out in Tim McLean's March 2015 Auth0 research, patched in libraries including node-jsonwebtoken and PyJWT, and still surface in pentest reports a decade later whenever a team rolls custom JWT-decoding logic instead of pinning an expected algorithm.

What is JWT algorithm confusion?

JWT algorithm confusion is an authentication bypass that occurs when a server's JWT verification code trusts the alg value inside an attacker-supplied token instead of enforcing a single, pre-agreed algorithm. A JWT has three base64url-encoded segments — header, payload, signature — separated by dots, and the header contains a JSON object like {"alg":"RS256","typ":"JWT"}. That header is not covered by any external integrity check before verification runs; the token's own claim about how it was signed is what many libraries used to decide how to verify it. This maps to CWE-347 (Improper Verification of Cryptographic Signature) and CWE-345 (Insufficient Verification of Data Authenticity). The core design mistake is letting untrusted input select the security control that is supposed to validate that same input, which is why RFC 8725, the JWT Best Current Practices document published by the IETF in February 2020, explicitly instructs implementers to "not trust unverified data" including the alg header and to pin the expected algorithm at the application layer.

How does the RS256-to-HS256 downgrade attack actually work?

The RS256-to-HS256 attack works by feeding a server its own public RSA key back as if it were an HMAC shared secret, producing a signature the server's own code will accept as valid. In a normal RS256 flow, the issuer signs tokens with a private key and any verifier checks the signature with the corresponding public key, which is often published openly at a JWKS endpoint such as /.well-known/jwks.json or embedded directly in client-side JavaScript. HS256, by contrast, uses one shared secret for both signing and verifying. If the verification code branches on the token's own alg field — "if alg is HS256, call hmac_verify(payload, secret); if RS256, call rsa_verify(payload, public_key)" — an attacker can take the public key text (PEM or JWKS format), forge a new token header claiming alg: HS256, and HMAC-sign the payload using that public key string as the secret. A server that dynamically selects its verification function from the incoming header will run the HS256 branch, treat the known public key as the "secret," and confirm the forged signature. This exact scenario is reproducible today in PortSwigger's Web Security Academy lab "JWT authentication bypass via algorithm confusion," which walks through extracting a public key and forging admin-level tokens against a deliberately vulnerable app.

Why did the alg: none bypass work in older JWT libraries?

The alg: none bypass worked because early JWT libraries treated none as a legitimate, if unusual, signing algorithm rather than an explicit red flag, and skipped signature verification when they saw it. The JWT specification (RFC 7519, May 2015) does define none as an allowed algorithm value for cases where a token's integrity is guaranteed by a different mechanism, such as TLS alone — but several implementations extended that leniency into production verification paths without a matching secure default. An attacker who intercepted or fabricated a token simply changed the header to {"alg":"none"}, stripped the signature segment to an empty string, and re-encoded the header and payload. Vulnerable code paths accepted the token outright because they branched on "if alg == none, skip signature check" with no accompanying check for who was allowed to request that. This is precisely the flaw Auth0's Tim McLean disclosed on March 31, 2015, across multiple language ecosystems, and it was fixed in the Node.js jsonwebtoken package as CVE-2015-9235, where versions prior to 4.2.2 did not restrict which algorithms a verifier would honor.

Which CVEs and real incidents show this flaw actually gets exploited?

CVE-2015-9235 and CVE-2017-11424 are the two most cited examples, and both trace back to the same 2015 disclosure wave. CVE-2015-9235 affected the npm jsonwebtoken library before version 4.2.2, where calling jwt.verify() without explicitly restricting algorithms allowed both the none-algorithm bypass and RS256/HS256 confusion. CVE-2017-11424 affected PyJWT prior to 1.5.2 (released May 2017), where the library's decode() function did not enforce the caller-specified algorithms allowlist strictly enough, again enabling algorithm-substitution attacks against RS256-issued tokens. Beyond these two libraries, the pattern has been documented against Java (java-jwt, jjwt), PHP (php-jwt), and Ruby JWT implementations in the same 2015 research round, which is why RFC 8725 five years later devoted an entire numbered recommendation ("Section 3.1: Perform Algorithm Verification") to closing this exact gap industry-wide. OWASP's API Security Top 10 (2023 edition) folds JWT misconfiguration, including algorithm-trust failures, under API2:2023 Broken Authentication, reflecting how often this shows up in API-first architectures where JWTs are the primary session mechanism rather than a supplementary token.

How can security teams detect and test for algorithm confusion in their own APIs?

Security teams detect algorithm confusion by attempting to forge a token using a downgraded or disabled algorithm and checking whether the server still accepts it, rather than by reading source code alone. Practical steps: capture a legitimate RS256 token and its corresponding public key (from the app's JWKS endpoint or embedded config); use a tool such as jwt_tool or Burp Suite's JWT extension to re-sign the payload as HS256 using the public key text as the HMAC secret, and submit it to the protected endpoint; separately, craft a second token with alg: none and an empty signature segment and submit that too. If either forged token is accepted, the verification code is trusting the header instead of enforcing a pinned algorithm — a finding that should be treated as critical since it typically leads to full authentication bypass or privilege escalation (for example, forging a token with role: admin in the payload). Static review should confirm that verification calls always pass an explicit, hardcoded algorithms list (e.g., algorithms=["RS256"] in PyJWT, algorithms: ['RS256'] in node-jsonwebtoken) rather than deriving the algorithm from the token itself, and that none is never in that allowlist.

How Safeguard Helps

Safeguard identifies JWT libraries and authentication middleware pulled into your codebase through automated SBOM generation and ingest, flagging versions with known algorithm-confusion history like pre-4.2.2 jsonwebtoken or pre-1.5.2 PyJWT the moment they enter a build. Reachability analysis then confirms whether the vulnerable verify() or decode() call path is actually exercised by an internet-facing authentication flow, so security teams triage the handful of exploitable instances instead of every dependency match. Griffin AI reviews the surrounding verification code for the underlying pattern — algorithm selection driven by untrusted header input — even in custom JWT-handling code that wouldn't show up in a CVE database at all. Where the fix is well-defined, such as replacing dynamic algorithm dispatch with a hardcoded allowlist, Safeguard opens an auto-fix pull request so engineering can ship the correction without a manual patch cycle.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.