JWT Algorithm Confusion: RS256 to HS256, Psychic Signatures, and alg:none on Production APIs
Every JWT security guide arrives at the same paragraph: "never trust the alg header." The advice is correct, but incomplete. It collapses 3 mechanically distinct attack classes into a single warning. Testers end up chasing the rarest variant and missing the 2 that bypass authentication on real production systems.
Three Classes, Three Exploitability Profiles
Merging alg:none, RS256→HS256 confusion, and ECDSA implementation bugs under a single "algorithm confusion" label produces defective testing strategies. Testers find the rarest variant and skip the realistic ones in production.
alg:none is eliminated by any post-2015 JWT library when correctly configured. The PortSwigger scanner (Burp Suite) detects it in seconds with trivial wordlists. RS256→HS256 confusion persists in production because it requires explicit algorithm pinning on the server, a configuration many applications skip. H1 reports confirm active bounties between 2022 and 2024 (see HackerOne Evidence section).
CVE-2022-21449 exploits EC arithmetic in the JVM, not the JWT specification. Every Java application using ES256/ES384/ES512 on JDK 15-18 before the April 2022 patch was vulnerable, regardless of library configuration. RFC 8725 (JWT BCP, 2020) mandates algorithm pinning on the server but says nothing about bugs at the EC implementation layer.
The class determines the attack surface. Testers who run alg:none first and stop there miss the 2 highest-impact classes.
RS256 to HS256: When the Public Key Is the HMAC Secret
RS256→HS256 confusion is the highest-probability class in production API testing. The public key, used as an HMAC-SHA256 secret, is intentionally exposed by the server via the JWKS endpoint. That is what makes this attack different: the required material is publicly available by design.
The attack vector starts with a GET on /.well-known/jwks.json. The attacker extracts the RSA public key n and e values. They then sign a forged token with HS256 using the raw bytes of that key as the HMAC secret. On the server, the library receives verify(token, publicKey). If the algorithm is not pinned on the server, the same publicKey is used as the secret for the HS256 path, producing 0=0 verification.
Tim McLean (Auth0, 2015) documented this pattern in node-jsonwebtoken, pyjwt, namshi/jose, and php-jwt, all pre-2.x versions. CVE-2016-10555 affected jwt-simple for Node.js. The fix in jsonwebtoken came in v4.2.2, requiring an explicit algorithms option on the server side.
CVE-2022-29217 (CVSS 7.5) affected PyJWT versions 1.5.0 through 2.3.0. Non-blocked public key formats were accepted in the context of algorithm confusion. The fix landed in version 2.4.0. CVE-2024-33663 (CVSS 7.4) affected python-jose through version 3.3.0. Confusion with the OpenSSH ECDSA key format enabled the same attack class, fixed only in 3.3.1.
To exploit, ticarpi's jwt_tool runs:
python3 jwt_tool.py TOKEN -X k -pk public.pem
The -X k flag signs the token with HMAC-SHA256 using the public key bytes as the secret. jwt_tool also downloads and processes the key directly from the JWKS endpoint, eliminating the manual PEM reconstruction step.
H1 #3800870 (8x8, 2024) confirmed the pattern in production. The API at connect.8x8.com accepted HS256 tokens signed with the RSA public key as the HMAC secret. The JWT verifier did not apply algorithm pinning. Admin tokens were forged without the RSA private key.
Psychic Signatures: CVE-2022-21449 and What r=s=0 Does to ECDSA
CVE-2022-21449 is categorically different from algorithm confusion. It is a missing bounds check in Java's EC arithmetic. It makes any ECDSA signature with r=0, s=0 valid on JDK 15-18 before the April 2022 patch. The practical result: forged ES256/ES384/ES512 JWTs pass verification regardless of algorithm pinning in the library.
The root cause originates in Java 15. The EC code was rewritten from C++ to Java. The new implementation omitted the check that r and s must be non-null and less than the curve order n. The mathematical effect is direct. ECDSA verification computes (r * s⁻¹ mod n) * G + (hash * s⁻¹ mod n) * Q = R. With r=s=0, all intermediate terms zero out and produce 0=0. That equality holds regardless of the message or public key.
Affected versions were Oracle JDK 17.0.2, 18, and GraalVM Enterprise Edition 20.3.5, 21.3.1, and 22.0.0.2. Oracle assigned CVSS 7.5 in the April 2022 CPU. Neil Madden (ForgeRock), who discovered the vulnerability, published the disclosure on April 19, 2022, alongside the Oracle patch.
The scope extends beyond JWTs. Any Java service calling java.security.Signature.verify() with ECDSA was affected, including TLS certificates, OAuth/OIDC tokens, and ES256 JWTs. JFrog confirmed the bug was introduced when the EC code was ported from C++ to Java in Java 15. The PoC is a 64-byte zero signature. That token passes verification on vulnerable JVMs without any key material.
The distinction from RS256→HS256 confusion is critical. The attacker needs no public key, no JWKS endpoint access, and no library misconfiguration. The only condition is the target running unpatched JDK 15-18.
alg:none in Production: Why It Is Rarer Than Guides Suggest
alg:none is the most documented JWT vulnerability and the least likely to survive in production. Every relevant library blocked the algorithm by default after 2015. Every WAF and scanner detects it in under 1 second.
Tim McLean's 2015 disclosure triggered immediate patches in node-jsonwebtoken, pyjwt, jjwt, and php-jwt. All current versions reject alg:none unless explicitly re-enabled via a flag such as allowInsecureAlgorithm. The PortSwigger scanner detects this as a passive check. Dedicated wordlists include "None", "NONE", and "nOnE" to cover case-insensitive bypasses in careless parsers.
The actual production occurrence pattern is custom JWT parsers, legacy SDKs, or lab environments. H1 reports categorized as alg:none almost always involve custom parsers or unmaintained forks, not production versions of pyjwt/jsonwebtoken/jose. RFC 8725, section 3.1, is explicit: implementations must not accept tokens with algorithm none unless the application explicitly requires it.
Testing value still exists: 1 request, 30 seconds. Spending more than that signals a miscalibrated threat model.
HackerOne Evidence by Class
Disclosed reports on HackerOne show that RS256→HS256 confusion hit production authentication endpoints at companies with active security programs. The pattern: the server accepted tokens signed by the algorithm specified in the token header. No enforcement of the server-configured algorithm.
H1 #3800870 (8x8, 2024): the connect.8x8.com API accepted HS256 tokens signed with the RSA public key as the HMAC secret. The JWT verifier did not apply algorithm pinning. Admin tokens were forged without the RSA private key.
H1 #1210502 (Jitsi-meet/8x8, 2021): the Prosody XMPP module accepted the symmetric HS256 algorithm to validate conference room authorization JWTs. Forged tokens granted access to any protected room without key material. H1 #576504 (Revive Adserver, 2019): insecure token construction enabled authentication bypass in the password recovery flow, in /lib/OA/Dal/PasswordRecovery.php.
No recent high-severity H1 report targets alg:none on maintained library versions. This confirms the exploitability gap between classes and validates the priority order in the testing methodology.
Detection: From JWKS Recon to Confirmed Bypass
A 3-phase methodology maps the real attack surface instead of running 1 alg:none test and moving on.
Phase 1: JWKS recon. Run GET on /.well-known/jwks.json, /.well-known/openid-configuration, and /api/auth/keys. Extract kty (RSA or EC), alg, and kid. RSA keys enable the RS256→HS256 path. EC keys on unpatched JVMs enable Psychic Signatures.
Phase 2: algorithm negotiation. Send the valid token with the alg header modified to HS256 (if the server uses RS256). Then send with alg:none. A 401 on alg:none combined with a 200 on HS256 confirms vulnerable RS256→HS256. A 200 on both indicates 2 classes exploitable simultaneously.
Phase 3a: RS256→HS256 exploit. Extract n and e from JWKS. Reconstruct the PEM with openssl. Run:
python3 jwt_tool.py TOKEN -X k -pk public.pem
Check whether the admin endpoint returns 200. A 200 with a forged payload confirms authentication bypass.
Phase 3b: Psychic Signatures verification. If the target is Java (indicators: X-Powered-By: Undertow, Tomcat headers, .jsp extensions), build an ES256 token with r=s=0 signature. Verify the server runs unpatched JDK 15-18. The mere presence of a JVM header combined with ES256 justifies the test.
(MAGO team tool) automates phases 1 and 2. It checks the alg header against the accepted algorithm list and flags the key type exposure on the JWKS endpoint. It also tests whether the server accepts both RS256 and HS256 for the same token, logging each HTTP response for later analysis.
The operational conclusion is prioritization: test RS256→HS256 confusion on every API that exposes a JWKS endpoint. It is the realistic class in production. Check CVE-2022-21449 on Java services before assuming the algorithm is secure. Implementation bugs survive correct library configuration. Test alg:none last with a 30-second budget, not 30 minutes.
Top comments (0)