# PYTHON-JWT-SEC-004: JWT Exposed Credentials

> **Severity:** MEDIUM | **CWE:** CWE-522, CWE-312 | **OWASP:** A02:2021, A04:2021

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

## Description

This rule flags jwt.encode() calls for review to ensure that passwords, API keys,
or other secrets aren't being included in the JWT payload. JWT payloads are
base64-encoded, not encrypted. Anyone who intercepts the token -- from browser
DevTools, a proxy, server logs, or a database -- can decode the payload and read
every field in plaintext.

Developers sometimes include passwords in JWT payloads because it seems convenient
-- "the token is signed, so it's secure." Signing prevents tampering, not reading.
A signed JWT is like a sealed letter written in pencil: nobody can change it, but
anyone can read it.

The rule operates at audit level, flagging all jwt.encode() calls. This is because
the engine can't yet inspect dictionary keys in arguments to check specifically for
keys like "password", "secret", or "api_key". Future engine updates will add dict
key matching to reduce false positives.


## Vulnerable Code

```python
import jwt

# Vulnerable: password stored in JWT payload (base64-encoded, not encrypted)
payload = {"username": "admin", "password": "secret123", "role": "superuser"}
token = jwt.encode(payload, "key", algorithm="HS256")

# Vulnerable: credentials in payload
user_token = jwt.encode({"email": "user@example.com", "password": "hunter2"}, SECRET, algorithm="HS256")
```

## Secure Code

```python
import jwt
import os

SECRET = os.environ["JWT_SECRET_KEY"]

# SECURE: Only include non-sensitive identifiers in the payload
token = jwt.encode(
    {
        "user_id": 123,          # Opaque identifier, not a secret
        "role": "admin",         # Authorization claim
        "exp": expires_at,       # Expiration
        "iat": issued_at,        # Issued at
    },
    SECRET,
    algorithm="HS256"
)

# NEVER include passwords, API keys, secrets, SSNs, credit card numbers,
# or any PII that would cause harm if exposed.

```

## 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-004",
    name="JWT Exposed Credentials (Audit)",
    severity="MEDIUM",
    category="jwt",
    cwe="CWE-522",
    tags="python,jwt,credentials,audit,CWE-522",
    message="jwt.encode() detected. Ensure no passwords or secrets are in the payload.",
    owasp="A02:2021",
)
def detect_jwt_exposed_credentials():
    """Audit: detects jwt.encode() to review for credential exposure."""
    return JWTModule.method("encode")
```

## How to Fix

- Never put passwords, API keys, secrets, or PII in JWT payloads -- use opaque user IDs and look up sensitive data server-side
- If you need to include user attributes in the token, limit it to non-sensitive claims like user_id, role, and permissions
- Use JWE (JSON Web Encryption) if you genuinely need encrypted token payloads, but consider whether the data should be in the token at all
- Review your JWT payload structure with your security team -- the principle is minimum necessary claims
- Add automated checks in code review to catch sensitive keys being added to token payloads

## Security Implications

- **Password Exposure:** If a password is in the JWT payload, anyone who gets the token can read it.
JWTs appear in Authorization headers (visible in browser DevTools), server
access logs, reverse proxy logs, and error tracking tools. That's a lot of
places for a plaintext password to end up.

- **Credential Harvesting:** Tokens are stored in localStorage, cookies, and mobile app storage. A single
XSS vulnerability or compromised device can expose every JWT the user has
received -- and if those tokens contain passwords, the attacker gets credentials
for free.

- **Compliance Violations:** Storing passwords in cleartext (even base64-encoded) violates PCI DSS, GDPR,
and most security standards. Base64 is not encryption -- it's encoding. Auditors
and pen testers will flag this immediately.

- **Password Reuse Exploitation:** Users reuse passwords across services. A password leaked from a JWT can be used
in credential stuffing attacks against other services -- email, banking, cloud
accounts.


## FAQ

**Q: Why is base64 encoding not the same as encryption?**

Base64 is a reversible encoding scheme -- anyone can decode it without any key.
Run echo "eyJ1c2VyIjoiYWRtaW4ifQ" | base64 -d and you get {"user":"admin"}.
Encryption requires a secret key to decrypt. JWT payloads are base64-encoded,
which means they are readable by anyone who has the token.


**Q: What should I include in a JWT payload?**

Include only non-sensitive identifiers and authorization claims: user_id (opaque,
not email), role, permissions, exp (expiration), iat (issued at), iss (issuer).
Look up anything sensitive server-side using the user_id from the token.


**Q: Why does this rule flag all jwt.encode() calls?**

The engine can't yet inspect dictionary keys in function arguments. To check
specifically for keys like "password" or "secret" in the payload dict, it would
need dict key matching support. This is a known engine limitation tracked in
the product roadmap. For now, the rule flags all jwt.encode() calls for manual
review.


**Q: Should I use JWE instead of JWS for sensitive data?**

JWE (JSON Web Encryption) encrypts the payload, so it can't be read without the
decryption key. But first ask whether the data needs to be in the token at all.
Tokens get stored in browsers, logged by proxies, and cached by CDNs. Even
encrypted, minimizing the data in a token reduces your attack surface.


**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 difference between this rule and SEC-005 (exposed data)?**

SEC-004 is about credentials in the payload (passwords, API keys). SEC-005 is
about user-controlled input flowing into jwt.encode() through taint analysis --
it catches cases where request data (form fields, query parameters) ends up in
the token payload, which could expose user-submitted sensitive data.


## References

- [CWE-522: Insufficiently Protected Credentials](https://cwe.mitre.org/data/definitions/522.html)
- [CWE-312: Cleartext Storage of Sensitive Information](https://cwe.mitre.org/data/definitions/312.html)
- [RFC 8725: JSON Web Token Best Current Practices (Section 3.11)](https://tools.ietf.org/html/rfc8725#section-3.11)
- [OWASP JWT Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html)
- [RFC 7519: JSON Web Token (JWT) -- Claims](https://tools.ietf.org/html/rfc7519#section-4)
- [PyJWT Documentation](https://pyjwt.readthedocs.io/en/stable/)

---

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