# PYTHON-JWT-SEC-001: JWT Hardcoded Secret

> **Severity:** HIGH | **CWE:** CWE-798, CWE-522 | **OWASP:** A02:2021

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

## Description

This rule detects jwt.encode() calls in your codebase where the signing secret
might be hardcoded. A hardcoded JWT secret means anyone who reads your source code
-- a contractor, an ex-employee, anyone with access to your Git history -- can forge
valid tokens for any user. It also means you can't rotate the secret without
redeploying every service that uses it.

The rule works by matching all jwt.encode() calls through type-aware resolution of
the PyJWT library. It currently operates as an audit-level rule, meaning it flags
every jwt.encode() call for manual review. This is because the engine cannot yet
distinguish between a string literal argument (hardcoded) and a variable reference
(potentially safe). Future versions will add an is_literal() qualifier to filter
only calls where the secret argument is a string literal.

In practice, the fix is simple: move the secret to an environment variable, a
secrets manager, or a configuration file that isn't checked into version control.
For distributed systems, consider asymmetric signing (RS256/ES256) where the private
key never leaves the signing service.


## Vulnerable Code

```python
import jwt

# Vulnerable: hardcoded string as JWT signing secret
token = jwt.encode({"user": "admin"}, "hardcoded_secret", algorithm="HS256")

# Vulnerable: another hardcoded secret
auth_token = jwt.encode({"role": "superuser"}, "my_secret_key_123", algorithm="HS256")
```

## Secure Code

```python
import jwt
import os
from datetime import datetime, timedelta

# SECURE: Load secret from environment variable
SECRET = os.environ["JWT_SECRET_KEY"]

token = jwt.encode(
    {"user_id": 123, "role": "admin", "exp": datetime.utcnow() + timedelta(hours=1)},
    SECRET,
    algorithm="HS256"
)

# SECURE: Asymmetric signing -- private key from file, not from code
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-001",
    name="JWT Hardcoded Secret",
    severity="HIGH",
    category="jwt",
    cwe="CWE-522",
    tags="python,jwt,hardcoded-secret,credentials,OWASP-A02,CWE-522",
    message="Hardcoded string used as JWT signing secret. Use environment variables or key management.",
    owasp="A02:2021",
)
def detect_jwt_hardcoded_secret():
    """Detects jwt.encode() with hardcoded string secret."""
    return JWTModule.method("encode")
```

## How to Fix

- Move JWT signing secrets to environment variables or a secrets manager like AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager
- Use asymmetric algorithms (RS256, ES256) for distributed systems so the signing key never leaves the auth service
- Rotate secrets periodically and ensure your architecture supports rotation without downtime
- Add pre-commit hooks or CI checks to prevent secrets from being committed to version control
- Never log or include JWT secrets in error messages, stack traces, or debug output

## Security Implications

- **Token Forgery:** Anyone with the secret can create valid JWTs. An attacker who finds the secret
in your Git history, a Docker image layer, or a leaked backup can issue tokens
with any claims -- admin roles, any user ID, unlimited expiration.

- **No Secret Rotation:** Hardcoded secrets can't be rotated without a code change and redeployment.
If the secret is compromised, you need to deploy new code to every service.
With an environment variable or secrets manager, rotation is an ops task.

- **Credential Leakage via Source Code:** Source code gets shared -- open source, contractor handoffs, CI logs, error
messages. A secret in code is a secret waiting to leak. Environment variables
and secrets managers keep credentials out of the codebase entirely.

- **Compliance Violations:** PCI DSS, SOC 2, and ISO 27001 all prohibit hardcoded credentials. Auditors
specifically look for secrets in source code. This finding will fail a
compliance review.


## FAQ

**Q: Why does this rule flag my jwt.encode() call if I'm using an environment variable?**

This rule currently operates at audit level -- it flags all jwt.encode() calls for
review because the engine can't yet distinguish between a hardcoded string like
"my_secret" and a variable like SECRET that loads from os.environ. If your secret
comes from an environment variable or secrets manager, the finding is a false positive
you can safely dismiss. Future engine updates will add literal detection to eliminate
these false positives automatically.


**Q: Should I use HS256 or RS256 for JWT signing?**

It depends on your architecture. HS256 (symmetric) uses the same secret for signing
and verification -- simpler, but every service that verifies tokens needs the secret.
RS256 (asymmetric) uses a private key to sign and a public key to verify -- the
private key stays on the auth server, and any service can verify with just the public
key. For microservices, RS256 is usually the better choice because it reduces the
blast radius of a key compromise.


**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's the fastest way to fix this finding?**

Replace the hardcoded string with os.environ["JWT_SECRET_KEY"]. Set the environment
variable in your deployment config (Docker, Kubernetes secrets, .env file that's
gitignored). This takes about two minutes and fixes the vulnerability permanently.


**Q: Is a long random string safe enough as a JWT secret?**

Length matters, but location matters more. A 256-bit random secret is cryptographically
strong -- but if it's hardcoded in your source code, it's only as secure as your Git
access controls. Move it to an environment variable. Then yes, generate it with
python -c "import secrets; print(secrets.token_hex(32))" and you're good.


**Q: Does this rule work across multiple files?**

Yes. The QueryType matcher resolves jwt.encode() calls regardless of which file they
appear in. If you import jwt in one file and call jwt.encode() in another, the rule
still detects it.


## References

- [CWE-798: Use of Hard-coded Credentials](https://cwe.mitre.org/data/definitions/798.html)
- [RFC 8725: JSON Web Token Best Current Practices](https://tools.ietf.org/html/rfc8725)
- [OWASP Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/)
- [PyJWT Documentation](https://pyjwt.readthedocs.io/en/stable/)
- [OWASP: Credential Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
- [Python: os.environ for Configuration](https://docs.python.org/3/library/os.html#os.environ)

---

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