VNX-JWT-004: JWT algorithm explicitly set to 'none'
A JWT is signed with, or a verifier accepts, the 'none' algorithm: an algorithm/algorithms option naming 'none' or PyJWT algorithm=None (Python, JS/TS, Ruby), jose UnsecuredJWT, Auth0 Algorithm.none(), jjwt Jwts.parser().unsecured(), and golang-jwt SigningMethodNone or UnsafeAllowNoneSignatureType.
| Field | Value |
|---|---|
| Rule ID | VNX-JWT-004 |
| Severity | Critical |
| SARIF level | error |
| Kind | sast |
| Languages | Python, JavaScript / TypeScript, Ruby, Java, Kotlin, Go |
| CWE | CWE-327 |
| CAPEC | CAPEC-196 |
| ATT&CK | T1550.001 |
| CWSS vector | CWSS:1.0/TI:C/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:C/DI:H/EX:H/EC:N/P:W |
| Tags | jwt algorithm authentication none-alg |
| Source | internal/sast/rules/vnx-jwt-004.rego |
Overview
This rule detects JWT operations that explicitly use or accept the none algorithm: an algorithm/algorithms option whose value is or contains 'none' (any case, including a list split over lines) or PyJWT jwt.encode(..., algorithm=None) in Python, JavaScript/TypeScript and Ruby files that use a JWT library; ruby-jwt JWT.encode(payload, nil, 'none'); jose new UnsecuredJWT() / UnsecuredJWT.decode(); Auth0 Algorithm.none() and jjwt Jwts.parser().unsecured(); and golang-jwt SigningMethodNone used to sign and UnsafeAllowNoneSignatureType. Code that compares a token’s alg with 'none' to reject it is not reported. The none algorithm is defined in RFC 7519 as an unsecured JWT: the signature section of the token is empty and no cryptographic verification is performed. Any party who holds such a token (or manufactures one from scratch) can present it as valid. Libraries that accept none as a permitted algorithm during verification allow an attacker to forge arbitrary token payloads and be accepted as authenticated. This maps to CWE-327 (Use of a Broken or Risky Cryptographic Algorithm).
The none algorithm was originally included in the JWT specification to support use cases where JWTs are transmitted over already-authenticated channels and a signature would be redundant. In practice, its inclusion in production authentication flows is almost always a mistake or a deliberate attack. Several high-profile CVEs have been filed against JWT libraries that accepted none by default or allowed it to be specified in the token header.
Why This Matters
The none algorithm attack on JWTs is well documented: an attacker takes a valid token, decodes the header (which is just base64), changes the alg field to "none", re-encodes, and strips the signature. A vulnerable library accepts this token as valid because it sees alg: none and skips signature verification. This allows complete authentication bypass and identity spoofing.
CAPEC-196 (Session Credential Falsification through Forging) and MITRE ATT&CK T1550.001 (Use Alternate Authentication Material: Application Access Token) document this attack. It requires only a base64 decoder and text editor, no cryptographic knowledge or computation. Any production system that accepts none-algorithm tokens effectively has no authentication at all.
The fix is straightforward: always specify a strong algorithm explicitly and never include none in the list of accepted algorithms.
What Gets Flagged
# FLAGGED: jwt.encode with algorithm='none'
token = jwt.encode({"sub": user_id}, key, algorithm="none")
# FLAGGED: jwt.decode accepting 'none' as the only algorithm
payload = jwt.decode(token, key, algorithms=["none"])
// FLAGGED: jwt.sign with algorithm: 'none'
const token = jwt.sign({ sub: userId }, secret, { algorithm: "none" });
// FLAGGED: verifier accepts 'none' alongside a real algorithm
const payload = jwt.verify(token, key, { algorithms: ["RS256", "none"] });
// FLAGGED
t := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{"sub": user})
return jwt.UnsafeAllowNoneSignatureType, nil
// FLAGGED
private final Algorithm algorithm = Algorithm.none();
Remediation
-
Replace
"none"with a strong signing algorithm. UseHS256for symmetric (shared secret) use cases,RS256orES256for asymmetric (public/private key) use cases.# SAFE: strong symmetric algorithm import jwt token = jwt.encode({"sub": user_id, "exp": expiry}, SECRET_KEY, algorithm="HS256") # SAFE: decode specifies only strong algorithms payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])// SAFE: strong algorithm in sign and verify const token = jwt.sign({ sub: userId }, secretKey, { algorithm: "HS256", expiresIn: "1h", }); const payload = jwt.verify(token, secretKey, { algorithms: ["HS256"] }); -
For asymmetric tokens (RS256, ES256), keep the private key out of the codebase and load it from a secrets manager or environment variable at runtime.
// SAFE: RS256 with externally loaded private key const privateKey = process.env.JWT_PRIVATE_KEY; const token = jwt.sign({ sub: userId }, privateKey, { algorithm: "RS256" }); -
Explicitly exclude
"none"from thealgorithmslist in decode/verify calls even if not using it, to ensure the library cannot be tricked by a forged header.# SAFE: whitelist only specific algorithms, never include 'none' ALLOWED_ALGORITHMS = ["HS256", "HS384"] payload = jwt.decode(token, SECRET_KEY, algorithms=ALLOWED_ALGORITHMS)
References
- CWE-327: Use of a Broken or Risky Cryptographic Algorithm
- CAPEC-196: Session Credential Falsification through Forging
- MITRE ATT&CK T1550.001 – Use Alternate Authentication Material: Application Access Token
- RFC 7519 – JSON Web Token (JWT)
- jwt.io – JWT introduction and algorithm overview
- OWASP JWT Security Cheat Sheet
- Auth0 – Critical vulnerabilities in JWT libraries (the ‘none’ attack)
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-jwt-004
// nosec vnx-jwt-004
Or record an ignore rule: vulnetix ignore add --rule vnx-jwt-004 --reason "...". See Suppressing findings.