JWT vulnerabilities (CWE-287): algorithm confusion, secret brute-force, and key-source injection — all bypass authentication entirely when tokens are misconfigured.
TL;DR
hashcat -m 16500 against rockyou.txtemail vs immutable sub, ESC-01 admin injection) enables account takeover without touching the signatureJSON Web Tokens (JWTs) are the dominant stateless authentication mechanism in modern APIs and single-page applications. Each token is a base64url-encoded header.payload.signature triple that carries identity claims — sub, role, email, exp — and is self-verified by the server without consulting a central session store. The security model depends entirely on the server correctly validating the signature before trusting any claim.
JWT vulnerabilities arise when that validation is absent, incomplete, or manipulable. They fall under CWE-287 (Improper Authentication) at the category level, with sub-classes spanning CWE-347 (Improper Verification of Cryptographic Signature), CWE-89 (SQL Injection via kid), CWE-78 (OS Command Injection via kid), and CWE-345 (Insufficient Verification of Data Authenticity for key-source attacks). OWASP maps the category to A07:2021 (Identification and Authentication Failures) as the primary classification, with A02:2021 (Cryptographic Failures) and A03:2021 (Injection) as secondary.
The attacks documented here are not theoretical. Six CVEs with CVSS scores above 8.0 were disclosed in the 2025-2026 window alone — CVE-2026-22817 (Hono), CVE-2026-34950 (fast-jwt), CVE-2026-27804 (Parse Server), CVE-2026-23552 (Keycloak), CVE-2024-54150 (jose), and CVE-2024-48916 (Ceph) — across Node.js, Java, Python, and Go ecosystems simultaneously. According to the Wallarm API ThreatStats Report Q2 2025, algorithm confusion accounts for 23% of JWT CVEs, kid injection for 18%, and mutable claim drift for 14%.
A JWT token has three base64url-encoded segments separated by dots. The header declares the algorithm and key identifier. The payload carries the claims. The signature is a cryptographic binding.
The attack surface lives in every step of that flow:
header.alg rather than enforcing an allowlist, the attacker changes alg to none or to HS256 to control the verification algorithm.kid is used in a database query or filesystem path without sanitization, the attacker injects SQL or path traversal to make the server use an attacker-controlled key.jku, x5u, or embedded jwk/x5c fields are supported without allowlisting, the attacker provides a URL or key material pointing to their own cryptographic material.email or custom role fields may be tampered to escalate privileges.The canonical minimal attack — the alg:none bypass — requires just base64url encoding:
import base64, json
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
# Forge a token claiming admin role, no signature needed
header = b64url(json.dumps({"alg": "none", "typ": "JWT"}).encode())
payload = b64url(json.dumps({"sub": "attacker", "role": "admin", "exp": 9999999999}).encode())
forged = f"{header}.{payload}." # empty signature segmentGET /api/admin/users HTTP/1.1
Host: vulnerable.example.com
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhdHRhY2tlciIsInJvbGUiOiJhZG1pbiIsImV4cCI6OTk5OTk5OTk5OX0.Every optional field in the JWT header is a potential injection point. The following table maps parameters to their attack class and prevalence:
| Header | Attack Class | CVSS Base | Prevalence | Example CVE |
|---|---|---|---|---|
alg | Algorithm confusion, alg:none bypass | 9.3–9.8 | Very common | CVE-2015-9235, CVE-2026-22817 |
kid | Path traversal, SQLi, NoSQLi, CMDi, LDAPi | 8.1–9.8 | Common | CWE-89, CWE-78 |
jku | SSRF, remote JWKS hijack | 8.7–9.1 | Common | CVE-2018-0114, CWE-918 |
x5u | SSRF, certificate URL injection | 8.7 | Rare | CWE-918, CWE-295 |
x5c | Embedded self-signed certificate | 9.3 | Rare | CWE-347, CWE-295 |
jwk | Embedded public key trusted unconditionally | 9.3 | Common | CVE-2018-0114 |
crit | RFC 7515 §4.1.11 critical extension bypass | 7.5 | Emerging | CWE-347 |
cty | Content-type XXE/deserialization chain | 8.6 | Emerging | CWE-611, CWE-502 |
zip | JWE decompression bomb | 7.0 | Rare | CVE-2024-39689 |
The jwk, jku, and x5u header parameters should be disabled entirely in most applications. No application that issues tokens from a server-controlled key store needs to accept remotely-specified or embedded keys. If your JWT library supports these fields and you have not explicitly disabled them, you are vulnerable to CVE-2018-0114 class attacks.
The alg:none attack bypasses signature verification by telling the library that no signature algorithm was used. The library, trusting the header, skips HMAC/RSA verification and accepts any token with a valid structure. Case variants — None, NONE, nOnE, nonE, none (leading whitespace) — bypass simple lowercase string-match filters. CVE-2026-34950 in fast-jwt re-enabled this class via a regex bypass using leading whitespace.
The RS256→HS256 confusion attack is more sophisticated. RS256 verifies with a public key; HS256 verifies with a shared secret. By downgrading to HS256 and signing with the server's public RSA key (publicly available from JWKS or TLS), the attacker makes the server verify HS256 against its own known public key — and accept the token. See JWT alg:none Attack for full detail.
HS256, HS384, and HS512 use a symmetric key — the same secret is used to sign and verify. If the secret is a dictionary word, a short string, or a value under 32 bytes, offline brute-force with hashcat -m 16500 cracks it against common wordlists in seconds. Once recovered, the attacker signs any arbitrary token. See JWT Weak Secret.
The kid (key ID) parameter identifies which key to use for verification. Most implementations look it up in a database or resolve it as a file path. Unvalidated kid values enable SQL injection (x' UNION SELECT 'AAAAAAAAA' --), path traversal (../../dev/null), OS command injection, NoSQL injection, and LDAP injection. See JWT kid Injection.
The jwk header parameter optionally embeds the verification key directly in the token header. CVE-2018-0114 in node-jose demonstrated that libraries accepting embedded JWK headers trusted the attacker-supplied public key unconditionally. The attacker generates an RSA keypair, embeds their public key in the header, signs with their private key, and the server verifies against the embedded public key — accepting the token. See JWT jwk Header Injection.
x5c embeds a DER-encoded certificate chain in the header; x5u provides a URL from which to fetch the certificate. Both allow an attacker to substitute attacker-controlled X.509 material for the server's trusted certificate. x5u is simultaneously an SSRF vector — the server-side HTTP fetch reaches internal services. See JWT x5c / x5u Injection.
CVE-2026-22817 — Hono JWK Middleware (CVSS 8.2)
Hono's JWT middleware before the fix did not enforce an algorithm requirement when processing JWK tokens. A token with no alg field in the JWK caused the library to fall back to an unintended default, enabling algorithm confusion. The pattern matches the JWKSAlgAbsent class: JWKS endpoints returning keys without specifying the allowed algorithm create an implicit trust surface for alg confusion. Fixed by requiring explicit alg in middleware configuration.
CVE-2026-34950 — fast-jwt Algorithm Confusion Re-enable (CVSS 9.1)
fast-jwt originally patched CVE-2023-48223 using a regex /^[A-Z]+\d+$/ to block none. A 2026 bypass was found: adding a leading whitespace character ( HS256) caused the regex to not match, and the library fell back to accepting the header's algorithm field directly — re-enabling the original CVE for users on patched versions.
CVE-2026-27804 — Parse Server OAuth2 Algorithm Confusion (CVSS 9.8)
Parse Server's OAuth2 integration with Google and Apple accepted JWTs signed with a weak algorithm because the server fetched the public key from the provider's JWKS without enforcing the alg field. An attacker controlling a Google/Apple-format JWKS response could bypass authentication entirely.
CVE-2026-23552 — Keycloak Cross-Realm Token Relay (CVSS 9.1)
Keycloak allowed a token issued for realm A to be accepted by realm B when the application did not strictly validate the iss (issuer) claim. Organizations running multi-tenant Keycloak were exposed to cross-tenant account takeover by any user with an account on any realm.
CVE-2024-54150 — jose (Cisco) RS256→HS256 Confusion (CVSS 9.1) The jose library used in Cisco products accepted HS256-signed tokens when configured for RS256, using the RS256 public key as the HS256 secret. PentesterLab published a detailed writeup: "Another JWT Algorithm Confusion CVE-2024-54150".
CVE-2022-21449 — Java Psychic Signatures (CVSS 7.5) Java SE 15-18 accepted an ECDSA signature with r=0 and s=0 as valid for any message. Every JWT library built on Java's JCA (java-jwt, nimbus-jose-jwt, jose4j) on affected JVMs was completely bypassed. Discovered by Neil Madden (ForgeRock), fixed in the April 2022 CPU update.
CVE-2018-0114 — node-jose jwk Injection (CVSS 9.3)
The original and most replicated jwk injection CVE. node-jose trusted the public key embedded in the jwk JWT header unconditionally. Attacker generates RSA keypair, embeds public key in header, signs with private key — server accepts the token. The attack class has been rediscovered in dozens of libraries since.
Allianz/Salesforce Mutable Claim Drift (2025)
ShinyHunters exploited applications that identified users by the email JWT claim rather than the immutable sub claim. An attacker changed their registered email at the identity provider to match a victim's address. The IdP issued a new token with the victim's email. The vulnerable application executed SELECT * FROM users WHERE email = $1 using the JWT's email claim, granting full account takeover without touching cryptographic material.
The mutable claim drift pattern is underdetected in code review. Any database query using jwt_claims["email"] instead of jwt_claims["sub"] is a potential account takeover vector. The sub claim (RFC 7519 §4.1.2) is guaranteed immutable within an issuer; email, username, and preferred_username are mutable at the identity provider.
alg, kid, jku, x5u, jwk, and x5c fields.{"alg":"none","typ":"JWT"}, strip the signature (keep the trailing dot), resend. Test case variants: None, NONE, nOnE, nonE, none (leading space)./.well-known/jwks.json or via openssl s_client. Use jwt_tool: python3 jwt_tool.py "$TOKEN" -X k -pk pubkey.pem -I -pc role -pv admin. A 200 response confirms confusion.kid to ../../dev/null, then to x' UNION SELECT 'AAAAAAAAA' -- -. Sign with an empty secret. A successful response confirms injection.hashcat -m 16500 token.txt /usr/share/wordlists/rockyou.txt. If cracked, forge arbitrary tokens with the recovered secret.jwt_tool (v2.2.6+) runs playbook mode covering 20+ attacks:
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -M pb -npBurp Suite JWT Editor extension automatically detects jku, x5c, x5u, jwk, and kid injection points and provides one-click attack execution.
hashcat for HMAC secret recovery:
hashcat -m 16500 -a 0 captured_token.txt /usr/share/wordlists/rockyou.txt
hashcat -m 16500 -a 3 captured_token.txt ?a?a?a?a?a?a?a?a # 8-char bruteforceBreachVex detects JWT vulnerabilities through a multi-stage attack sequence: it discovers JWKS endpoints across standard paths, establishes a verified authentication oracle (triple-baseline: valid→200, no-token→401, garbage→401), tests all algorithm confusion variants including the 8 alg:none case permutations, probes key-source injection with out-of-band callbacks for jku/x5u SSRF, fuzzes kid with path-traversal and SQLi/CMDi payloads, and runs HMAC cracking on symmetric tokens. Every finding requires differential body validation before reporting.
The single most important defense is refusing to trust the JWT header's alg field:
# BAD — trusts the header's algorithm claim
import jwt
decoded = jwt.decode(token, key, algorithms=jwt.get_unverified_header(token)["alg"])
# GOOD — explicit server-side allowlist
ALLOWED_ALGORITHMS = ["RS256"]
decoded = jwt.decode(token, public_key, algorithms=ALLOWED_ALGORITHMS)// BAD — jsonwebtoken trusts header algorithm
const payload = jwt.verify(token, secret)
// GOOD — explicit algorithm allowlist
const payload = jwt.verify(token, secret, { algorithms: ['RS256'] })
// BAD — Hono CVE-2026-22817 pattern
import { jwt } from 'hono/jwt'
app.use('/api/*', jwt({ secret: process.env.JWT_SECRET }))
// GOOD — explicit algorithm
app.use('/api/*', jwt({ secret: process.env.JWT_SECRET, alg: 'HS256' }))# BAD — mutable claim, vulnerable to IdP email change ATO
user = db.users.find_one({"email": decoded["email"]})
# GOOD — immutable sub per RFC 7519 §4.1.2
user = db.users.find_one({"oidc_sub": decoded["sub"]})import re
# BAD — path traversal and SQLi vector
key_data = open(f"/keys/{header['kid']}").read()
# GOOD — strict allowlist regex + parameterized query
KID_PATTERN = re.compile(r'^[a-zA-Z0-9_-]{1,64}$')
if not KID_PATTERN.fullmatch(header.get("kid", "")):
raise ValueError("Invalid kid claim")
row = db.execute("SELECT k FROM keys WHERE id = $1", (header["kid"],)).fetchone()UNSAFE_HEADERS = {"jku", "x5u", "jwk", "x5c"}
def verify_jwt(token: str, expected_alg: str, public_key) -> dict:
unverified_header = jwt.get_unverified_header(token)
if UNSAFE_HEADERS & unverified_header.keys():
raise SecurityError("Unsafe JWT header parameters present")
if unverified_header.get("alg") != expected_alg:
raise SecurityError("Algorithm mismatch")
return jwt.decode(token, public_key, algorithms=[expected_alg])import secrets
# GOOD — cryptographically random 32-byte secret
JWT_SECRET = secrets.token_bytes(32)
# Node.js: crypto.randomBytes(32).toString('hex')
# BAD — all crackable with hashcat -m 16500 in under 60 seconds:
# "secret", "password", "jwt-secret", "12345678901234567890"For distributed systems (microservices, multiple backend instances), use asymmetric keys (RS256 or ES256). Only the signing service holds the private key; all verifying services hold only the public key. A compromised verifier cannot forge new tokens. Symmetric HMAC (HS256) requires every service to share the secret, making every service a potential signing oracle if compromised.
Algorithm confusion occurs when the server trusts the alg field in the JWT header rather than enforcing an allowlist. An attacker changes alg from RS256 to HS256 and signs the token using the server's public key as the HMAC secret — bypassing signature verification entirely. Libraries like PyJWT and jsonwebtoken fix this by requiring an explicit algorithms= parameter.
The alg:none attack (CVE-2015-9235) sets the algorithm header to 'none', producing an unsigned token with an empty signature. Libraries that trusted the header's algorithm claim accepted these tokens as valid. Case variants — None, NONE, nOnE, nonE — bypass simple string-match filters. The fix is to reject any token where alg is not in an explicit server-side allowlist.
The kid (key ID) header parameter identifies which key to use for verification. If the application uses kid in a database query or file path without sanitization, an attacker can inject SQL to make the server sign with an attacker-chosen key, or inject a path traversal to make it sign with an empty or null key. This is covered under CWE-89 (SQL injection) and CWE-22 (path traversal).
JWK injection (CVE-2018-0114) embeds an attacker-controlled public key directly in the JWT header under the jwk field. Vulnerable libraries verify the token's signature against this embedded key rather than a server-side key store, allowing the attacker to sign any token with their own private key and have it accepted as valid.
x5u is a JWT header parameter specifying a URL from which to fetch the signing certificate. If the server fetches this URL without validation, an attacker sets x5u to point to their own certificate, signs the token with the matching private key, and the server accepts it. This is simultaneously a SSRF vector — the server-side fetch can reach internal services via the x5u URL.
Run: hashcat -m 16500 token.txt /usr/share/wordlists/rockyou.txt. Mode 16500 targets HMAC-SHA256/384/512 JWT tokens. Hashcat brute-forces the HMAC secret offline without contacting the server. Secrets under 32 bytes are typically cracked in seconds to minutes against rockyou.txt. Once recovered, the attacker can forge any token with any claims using the known secret.
jwt_tool (v2.2.6+ by ticarpi) covers 20+ attack vectors in playbook mode (-M pb). Burp Suite JWT Editor extension auto-detects jku/x5c/x5u/jwk/kid injection points. hashcat -m 16500 handles HMAC brute-force. sig2n derives RSA public keys from two valid token signatures. nuclei has jwt-specific templates in http/misconfiguration/jwt-*. jwt.io decodes and inspects tokens manually.
Major recent CVEs: CVE-2026-34950 (fast-jwt, CVSS 9.1 — algorithm confusion regex bypass), CVE-2026-22817 (Hono JWK middleware, CVSS 8.2), CVE-2026-27804 (Parse Server, CVSS 9.8), CVE-2026-23552 (Keycloak cross-realm, CVSS 9.1), CVE-2024-54150 (jose Cisco, CVSS 9.1), CVE-2024-48916 (Ceph RadosGW, CVSS 9.8), CVE-2022-21449 (Java Psychic Signatures, CVSS 7.5), CVE-2018-0114 (node-jose jwk injection, CVSS 9.3).
RS256 uses RSA asymmetric signing — a private key signs, the public key verifies. HS256 uses a shared HMAC secret for both signing and verification. A vulnerable library that trusts the header's alg field can be made to verify an HS256 signature using the RS256 public key — which is publicly known. The attacker signs a forged token with HS256 using the server's RSA public key as the HMAC secret, and the server accepts it.
Mutable claim drift occurs when an application identifies users by the email claim (which can be changed at the identity provider) instead of the immutable sub claim (RFC 7519 §4.1.2). An attacker registers at an IdP, changes their email to match a victim's email, receives a new ID token with that email, and the application maps them to the victim's account. The Allianz/Salesforce ShinyHunters pattern in 2025 exploited exactly this.
Keycloak CVE-2026-23552 allowed a token issued for realm A to be accepted by realm B because the iss (issuer) claim was not strictly validated per-realm. An attacker with an account on realm A could forge access to realm B services by relaying a valid token. The fix enforces strict issuer matching against the expected realm URL.
HMAC JWT secrets must be at least 32 bytes (256 bits) of cryptographically random data. Use: Python: secrets.token_bytes(32). Node.js: crypto.randomBytes(32).toString('hex'). Never use human-readable strings, passwords, or predictable values as JWT secrets. Rotate secrets every 90 days and maintain two active KIDs during rotation.
Cookies with HttpOnly, Secure, SameSite=Strict, and the __Host- prefix are safer than localStorage. localStorage is accessible to JavaScript — any XSS vulnerability can exfiltrate tokens stored there. HttpOnly cookies are immune to JavaScript access. Add Cache-Control: no-store on all token endpoints to prevent caching.
JWE supports DEFLATE compression via the zip header parameter. CVE-2024-39689 in the jose Python library allowed an unauthenticated attacker to craft a JWE with a massively inflated decompressed payload. Processing it exhausted server memory, causing denial of service. Mitigation: reject JWE tokens from untrusted sources or disable zip compression support.
CVE-2022-21449 affected Java SE 15-18: the ECDSA signature verifier accepted a signature where r=0 and s=0 as valid for any message. A forged JWT with a blank ECDSA signature passed verification on Java 15-18 regardless of the actual payload. Fixed in the April 2022 CPU update. Any Java application verifying ECDSA-signed JWTs on those versions was completely bypassed.
JWT vulnerabilities map to three OWASP Top 10 2021 categories: A02 (Cryptographic Failures) covers alg confusion, weak secrets, and key-source attacks. A07 (Identification and Authentication Failures) covers token forgery bypassing authentication. A03 (Injection) covers kid SQLi, kid CMDi, and kid LDAPi variants. The primary mapping is A07 with A02 as co-classification.