# PYTHON-JWT-SEC-002: JWT None Algorithm

> **Severity:** CRITICAL | **CWE:** CWE-327, CWE-345 | **OWASP:** A02:2021

- **Language:** Python
- **Category:** JWT
- **URL:** https://codepathfinder.dev/registry/python/jwt/PYTHON-JWT-SEC-002
- **Detection:** `pathfinder scan --ruleset python/PYTHON-JWT-SEC-002 --project .`

## Description

This rule detects jwt.encode() calls that explicitly set algorithm="none". The "none"
algorithm produces unsigned JWT tokens -- the token has no signature at all, which
means anyone can modify the payload (change user IDs, add admin roles, extend
expiration) and the receiving service has no way to tell.

This is one of the most exploited JWT vulnerabilities. The "none" algorithm was
originally intended for cases where the token integrity was already guaranteed by
the transport layer (e.g., mutual TLS). In practice, it became an attack vector:
libraries that accepted "none" as a valid algorithm allowed attackers to strip the
signature from a legitimately signed token and re-submit it.

The rule uses .where("algorithm", "none") for precise matching. It only fires when
the algorithm keyword argument is literally "none" -- it will not flag jwt.encode()
calls using HS256, RS256, ES256, or any other real algorithm. Zero false positives
on properly configured encode calls.


## Vulnerable Code

```python
import jwt

# Vulnerable: encode with algorithm="none" disables signature
token = jwt.encode({"user": "admin"}, "", algorithm="none")

# Vulnerable: using none algorithm with payload
unsafe_token = jwt.encode({"role": "superuser"}, "key", algorithm="none")
```

## Secure Code

```python
import jwt
import os

SECRET = os.environ["JWT_SECRET_KEY"]

# SECURE: Use HS256 with a strong secret
token = jwt.encode(
    {"user_id": 123, "role": "user"},
    SECRET,
    algorithm="HS256"
)

# SECURE: Use RS256 for distributed systems
with open("private_key.pem", "rb") as f:
    private_key = f.read()
token = jwt.encode({"sub": "user123"}, private_key, algorithm="RS256")

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets

class JWTModule(QueryType):
    fqns = ["jwt"]


@python_rule(
    id="PYTHON-JWT-SEC-002",
    name="JWT None Algorithm",
    severity="CRITICAL",
    category="jwt",
    cwe="CWE-327",
    tags="python,jwt,none-algorithm,weak-crypto,OWASP-A02,CWE-327",
    message="JWT with algorithm='none' disables signature verification. Use HS256 or RS256.",
    owasp="A02:2021",
)
def detect_jwt_none_algorithm():
    """Detects jwt.encode() with algorithm='none'."""
    return JWTModule.method("encode").where("algorithm", "none")
```

## How to Fix

- Never use algorithm="none" in production code -- there is no legitimate use case for unsigned tokens in a web application
- Use HS256 for single-service architectures where the same service signs and verifies tokens
- Use RS256 or ES256 for microservice architectures where multiple services need to verify tokens
- On the verification side, always pass an explicit algorithms list to jwt.decode() to prevent algorithm confusion attacks
- Add a linter rule or pre-commit check to catch "none" algorithm usage before it reaches production

## Security Implications

- **Complete Token Forgery:** With algorithm="none", there is no signature. An attacker can create a JWT
with any payload -- admin privileges, any user identity, any expiration date --
and the server will accept it if it doesn't enforce algorithm verification.

- **Privilege Escalation:** An attacker takes a valid low-privilege token, decodes the payload (JWT payloads
are just base64), changes "role" from "user" to "admin", re-encodes with
algorithm="none", and submits it. If the server accepts unsigned tokens, the
attacker is now admin.

- **Authentication Bypass:** An unsigned token can impersonate any user. The attacker doesn't need the
victim's password, the signing key, or any other credential. They just need
to know the expected payload structure.


## FAQ

**Q: Why is algorithm="none" so dangerous?**

Because it removes the only thing that makes a JWT trustworthy -- the signature.
JWTs are just base64-encoded JSON. Anyone can read and create them. The signature
is what proves the token was created by your server. Remove the signature and the
token is just a JSON string that anyone can modify.


**Q: Does this rule also detect "none" in jwt.decode(algorithms=["none"])?**

Not currently. The rule precisely matches jwt.encode() with algorithm="none". The
decode variant uses algorithms (plural) which takes a list value like ["none"].
Matching values inside list arguments requires a contains() qualifier that the
engine doesn't support yet. This is tracked as a product roadmap item.


**Q: Is there ever a legitimate use of algorithm="none"?**

In theory, RFC 7518 defines "none" for cases where integrity is provided by the
transport layer. In practice, there is no real-world web application scenario
where unsigned tokens are safe. Even in testing, use a test-specific HS256 secret
instead of disabling signing entirely.


**Q: Can an attacker change a signed token to use algorithm="none"?**

Yes -- this is the classic algorithm confusion attack. The attacker takes a valid
RS256-signed token, changes the header to {"alg": "none"}, strips the signature,
and submits it. If the server's jwt.decode() doesn't enforce an algorithms whitelist,
it will accept the unsigned token. This is why you should always pass
algorithms=["RS256"] (or whichever algorithm you use) to jwt.decode().


**Q: How do I run this rule in CI/CD?**

Run: pathfinder ci --ruleset python/jwt --project .
It outputs SARIF, JSON, or CSV. On GitHub, it posts inline review comments directly
on pull requests pointing to the exact lines. No dashboard needed.


**Q: What algorithm should I use instead?**

For most applications, HS256 (HMAC-SHA256) is the right default. It's fast, widely
supported, and secure with a 256-bit key. For microservices where you want to
distribute verification without sharing the signing key, use RS256 (RSA-SHA256) or
ES256 (ECDSA-P256). ES256 produces smaller tokens than RS256.


## References

- [CWE-327: Use of a Broken or Risky Cryptographic Algorithm](https://cwe.mitre.org/data/definitions/327.html)
- [RFC 8725: JSON Web Token Best Current Practices (Section 3.2 - Use and Verify Algorithm)](https://tools.ietf.org/html/rfc8725#section-3.2)
- [OWASP JWT Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html)
- [Auth0: Critical Vulnerabilities in JSON Web Token Libraries](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/)
- [PyJWT Algorithm Documentation](https://pyjwt.readthedocs.io/en/stable/algorithms.html)
- [RFC 7518: JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518)

---

Source: https://codepathfinder.dev/registry/python/jwt/PYTHON-JWT-SEC-002
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
