JWT algorithm confusion attacks are back — and Q1 2026 has seen a cluster of critical CVEs across major frameworks and libraries. The root cause is always the same: trusting the attacker-controlled alg field in the JWT header to select the signature verification algorithm.
This guide explains exactly how these attacks work, walks through the three most impactful 2026 CVEs, and gives you concrete, language-specific fixes you can apply today.
How JWT Signature Verification Is Supposed to Work
A signed JWT contains three Base64url-encoded sections: header.payload.signature. The header specifies the algorithm — for example, {"alg": "RS256"} for RSA-SHA256. Your server holds the public key of the issuer and uses it to verify the signature over header.payload.
The critical assumption: your server decides what algorithm to use, not the client.
The Attack: Two Variants
Variant 1 — The none Algorithm Bypass
The JWT specification originally allowed alg: "none" to indicate unsigned tokens for trusted environments. Attackers craft a token with:
{"alg": "none", "typ": "JWT"}
Strip the signature, leaving a trailing dot: header.payload.
Vulnerable libraries that don’t explicitly reject none will accept this as valid. Even when a library blocks the literal string "none", attackers bypass it with case variants: nOnE, NoNE, NONE. This is an algorithmic check, not a semantic one — any string comparison without lowercasing is vulnerable.
Detection: In your JWT decode step, reject immediately if alg is none (case-insensitive) or if the signature segment is empty.
Variant 2 — RS256 → HS256 Key Confusion
This is the more dangerous variant and the one powering the 2026 CVE cluster.
RSA (RS256) uses asymmetric cryptography: the issuer signs with a private key, and verifiers use the corresponding public key. HMAC (HS256) uses a symmetric shared secret — the same key signs and verifies.
Attackers exploit vulnerable libraries that accept the alg from the token header:
- Attacker fetches the server’s public key from the JWKS endpoint (
/.well-known/jwks.json) — this is public information, by design. - Attacker creates a JWT with
"alg": "HS256"in the header and arbitrary claims in the payload. - Attacker signs the JWT using the server’s public key as the HMAC secret.
- Server receives the token. Vulnerable library reads
alg: HS256from the header, selects HMAC verification, uses its own public key as the secret, and — because the math is identical — the signature validates successfully.
The server has been tricked into treating a public key as a shared secret. Authentication is completely bypassed.
CVE-2026-22817: Hono JWT Middleware (CVSS 8.2)
Affected: Hono versions before 4.11.4, running on Cloudflare Workers, Deno, Bun, and Node.js.
Vulnerability: Hono’s JWT middleware derived the verification algorithm from the incoming token’s alg header without pinning it to an expected value. An attacker sending "alg": "HS256" with the RS256 public key as secret could forge tokens for any user.
Fix: Upgrade to Hono ≥4.11.4. The patched middleware requires explicit algorithm configuration:
// Vulnerable (before 4.11.4) — alg inferred from token header
app.use('/api/*', jwt({ secret: publicKey }))
// Fixed (4.11.4+) — alg must be explicitly pinned
app.use('/api/*', jwt({ secret: publicKey, alg: 'RS256' }))
If you cannot upgrade immediately, add a pre-verification check that rejects any token whose decoded header alg does not match your expected algorithm.
CVE-2026-27804: Parse Server OAuth Adapters (CVSS 9.3)
Affected: Parse Server < 8.6.3 and ≥ 9.0.0 < 9.3.1-alpha.4, with Google, Apple, or Facebook OAuth adapters enabled.
Vulnerability: Parse Server’s OAuth adapters extracted the alg field from the incoming JWT header and passed it directly to the verification function:
// VULNERABLE CODE PATTERN (simplified)
const { alg: algorithm } = authUtils.getHeaderFromToken(token);
jwtClaims = jwt.verify(token, googlePublicKey, {
algorithms: algorithm, // attacker controls this!
audience: clientId,
});
An attacker could set alg: "none" to strip the signature entirely, or alg: "HS256" to sign with Google’s public key as HMAC secret. Either path enabled complete account takeover — including admin accounts.
Fix: Upgrade to Parse Server 8.6.3 or 9.3.1-alpha.4. The fix hardcodes RS256:
// PATCHED
jwtClaims = jwt.verify(token, signingKey, {
algorithms: ['RS256'], // hardcoded, not user-controlled
audience: clientId,
});
The patched version also replaces the custom key fetcher with jwks-rsa, which rejects tokens with unknown key IDs.
If you cannot upgrade: Immediately disable Google/Apple/Facebook OAuth adapters until you can patch.
CVE-2026-23993: HarbourJwt Go Library (unknown alg Bypass)
Affected: HarbourJwt Go library.
Vulnerability: When the library encountered an unrecognized algorithm string (e.g., "zzz", "banana", or any typo), its GetSignature() function returned an empty byte slice instead of an error. Many callers interpreted an empty signature as “no signature needed” and accepted the token.
// Attacker token header: {"alg": "zzz"}
// HarbourJwt returns: signature = []byte{} (empty, not error)
// Caller: if len(signature) == 0 { return nil } // WRONG assumption
Fix: Upgrade to the patched HarbourJwt release. In any Go JWT library, always use jwt.WithValidMethods() to whitelist allowed algorithms:
token, err := jwt.Parse(tokenString, keyFunc,
jwt.WithValidMethods([]string{"RS256"}),
)
if err != nil {
// Reject — includes unknown alg, signature failure, expiry, etc.
return nil, err
}
Never check len(signature) == 0 as a “no-op” condition — treat it as an error.
CVE-2026-23552: Apache Camel Keycloak Cross-Realm Bypass (CVSS 9.1)
Affected: Apache Camel 4.15.0 through 4.17.x (KeycloakSecurityPolicy component).
Vulnerability: A different class of JWT validation failure — this one involves the iss (issuer) claim rather than the alg field. The KeycloakSecurityPolicy component validated that tokens were signed correctly but did not verify that the iss claim matched the configured Keycloak realm.
Result: A valid token from any Keycloak realm (including attacker-controlled ones) was accepted by services configured for a completely different realm. This breaks multi-tenant isolation entirely.
Fix: Upgrade Apache Camel to ≥4.18.0, which adds strict iss claim validation against the configured realm URL.
For Keycloak-integrated services generally, always validate the iss claim:
// Java (Keycloak adapter / jjwt)
Jwts.parser()
.requireIssuer("https://auth.example.com/realms/production")
.setSigningKeyResolver(keyResolver)
.parseClaimsJws(token);
Language-Specific Fixes: Algorithm Pinning
Node.js (jsonwebtoken)
// ❌ Vulnerable — algorithm derived from token
const decoded = jwt.verify(token, publicKey);
// ✅ Fixed — algorithm pinned server-side
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // only accept RS256; reject HS256, none, etc.
});
Python (PyJWT)
import jwt
# ❌ Vulnerable
payload = jwt.decode(token, public_key)
# ✅ Fixed — algorithms list required in PyJWT ≥2.x
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="my-service"
)
Go (golang-jwt/jwt)
import "github.com/golang-jwt/jwt/v5"
// ✅ Fixed — WithValidMethods enforces the whitelist
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Double-check: reject HS256 even before the library check
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return rsaPublicKey, nil
}, jwt.WithValidMethods([]string{"RS256"}))
Java (jjwt 0.12+)
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
// ✅ Fixed — requireSignedWith enforces algorithm + key type
try {
Claims claims = Jwts.parser()
.requireIssuer("https://auth.example.com/realms/production")
.verifyWith(rsaPublicKey) // enforces RS256 implicitly via key type
.build()
.parseSignedClaims(token)
.getPayload();
} catch (JwtException e) {
// Reject: expired, wrong issuer, bad signature, wrong algorithm, etc.
}
Checklist: Is Your JWT Verification Vulnerable?
Run through this checklist for every service that validates JWTs:
- Algorithm pinned server-side —
algorithms: ['RS256']or equivalent, never derived from the token header -
nonerejected explicitly — reject any token whosealgheader isnone(case-insensitive check) - Issuer (
iss) validated — verified against the exact expected realm/tenant URL - Audience (
aud) validated — verified against your service identifier - JWKS key ID (
kid) validated —jwks-rsaor equivalent rejects unknown key IDs - Library up to date — Hono ≥4.11.4, Parse Server ≥8.6.3, Apache Camel ≥4.18.0
- Logs inspect
algheader — alert on unexpected algorithm values in production
Using IAMDevBox Tools to Detect Suspicious JWTs
Our JWT Decoder tool shows the raw alg header value from any token. If you receive a token from an OAuth provider and the decoder shows HS256 when you expected RS256 — or none — you’re looking at a potential algorithm confusion attack.
The SAML Decoder tool similarly helps inspect SAML assertions for signature algorithm values (<ds:SignatureMethod>), which can be downgraded in XML signature wrapping attacks.
Why This Keeps Happening
JWT algorithm confusion is a category of vulnerability that has existed since 2015 (the original none algorithm bypass), yet new CVEs keep appearing. The reason: cryptographic verification has a deceptively simple API surface that hides complex invariants.
A call like jwt.verify(token, key) looks simple, but it encodes a critical requirement: the caller must not allow the token to influence how key is used. When library APIs make it easy to pass the key without specifying the algorithm, developers naturally omit the algorithm parameter — and the library falls back to reading it from the token.
The fix is equally simple: always specify algorithms. Make it a code review requirement for any JWT verification call in your codebase. Use our JWT tools and the PKCE Generator for testing OAuth flows safely.
For more background on OAuth and token security, see our OAuth 2.0 Complete Developer Guide and Access Token Theft: Understanding and Mitigating the Threat.

