{"repro_id":"REPRO-2026-00381","version":6,"title":"IBM Langflow OSS 1.0.0 through 1.12.2 is vulnerable to remote unauthenticated code execution via OS command injection.","repro_type":"security","status":"published","severity":"critical","cvss_score":9.8,"description":"Langflow OSS 1.0.0-1.12.2 allows a remote attacker to execute arbitrary code due to improper neutralization of special elements used in an OS command. Fixed in 1.12.3 per IBM bulletin (one of 25 vulnerabilities patched; CVE-2026-93674 and CVE-2026-104334 are the two 9.8 criticals).","root_cause":"# CVE-2026-93674 — Root Cause Analysis\n\n## Summary\n\nLangflow OSS through 1.12.2 exposes `POST /api/v1/validate/code`, a \"validation-only\"\nendpoint whose backend (`lfx.custom.validate.validate_code`) calls\n`importlib.import_module()` on **every import statement** found in attacker-supplied\ncode. Importing a module executes its top-level code, so the endpoint runs arbitrary\nPython inside the Langflow server process during what is documented as a non-executing\nvalidation step. Combined with (a) the default single-user configuration\n(`LANGFLOW_AUTO_LOGIN=true`, which lets any network client mint a superuser token from\n`GET /api/v1/auto_login` with no credentials) and (b) a second remote code-evaluation\nsink (`POST /api/v1/custom_component`, which `exec()`s the attacker-supplied component\nclass body and can plant a malicious module into the server-writable\n`site-packages`), a remote unauthenticated attacker obtains arbitrary OS command\nexecution as the Langflow service user. The vulnerability is fixed in Langflow 1.12.3\nby commit `461506ac2f38f70a994b5140572b876448c11e4c` (\"fix(security): close\npathlib/io/codecs scanner bypass and stop validate_code from executing imports\",\nH1-3992099 / LE-2683), which replaces `import_module()` with\n`importlib.util.find_spec()` — locate-only, never executing module code.\n\n## Impact\n\n- **Package/component:** `langflow` / `langflow-base` / `lfx` (PyPI), specifically\n  `lfx.custom.validate.validate_code` behind the FastAPI route\n  `POST /api/v1/validate/code`.\n- **Affected versions:** 1.0.0 through 1.12.2 (IBM bulletin / NVD CPE range; the\n  vulnerable `importlib.import_module()` loop is present in v1.12.2 and removed in\n  v1.12.3).\n- **Risk level:** Critical (CVSS 9.8, AV:N/AC:L/PR:N/UI:N). Full remote,\n  unauthenticated OS command execution with the privileges of the Langflow service\n  account (in the official container: `uid=1000(user) gid=0(root)`), i.e. complete\n  compromise of flows, stored credentials/global variables, and any data readable by\n  the service.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** remote (unauthenticated) arbitrary code /\n  OS command execution (`code_execution`).\n- **Reproduced impact from this run:** remote unauthenticated OS command execution\n  through the real HTTP API of the digest-pinned official `langflowai/langflow:1.12.2`\n  image: the planted module ran `id` via `subprocess.check_output(..., shell=True)`\n  inside the server process; the output (`uid=1000(user) gid=0(root) groups=0(root)`)\n  was exfiltrated in-band in the HTTP 500 `detail` field of the very\n  `POST /api/v1/validate/code` response, and a unique per-attempt marker file was\n  written inside the container filesystem.\n- **Parity:** `full`.\n- **Not demonstrated:** nothing material — the claimed impact class was reproduced\n  end-to-end. (A persistent shell/pivot was not attempted; it is not required for\n  parity.)\n\n## Root Cause\n\n`src/lfx/src/lfx/custom/validate.py` (v1.12.2), function `validate_code(code)`:\n\n```python\n# Evaluate the import statements\nfor node in tree.body:\n    if isinstance(node, ast.Import):\n        for alias in node.names:\n            try:\n                importlib.import_module(alias.name)   # <-- EXECUTES module top-level code\n            except ModuleNotFoundError as e:\n                errors[\"imports\"][\"errors\"].append(str(e))\n```\n\n`importlib.import_module()` is not a lookup — it loads and **executes** the module.\nBecause the endpoint is reachable by any network client under the default\n`LANGFLOW_AUTO_LOGIN=true` configuration (the auto-login route issues a superuser\nJWT without credentials), an attacker who can place a Python file on any\n`sys.path` entry writable by the service account gets it executed by simply sending\n`{\"code\": \"import <module>\"}`. The official container runs as `uid=1000` and owns\n`/app/.venv/lib/python3.14/site-packages`, which is on `sys.path`, so the built-in\ncustom-component code-evaluation feature (`POST /api/v1/custom_component` →\n`build_custom_component_template()` → `exec()` of the class body) provides the\nfile-write primitive fully remotely:\n\n```python\nclass Planter(CustomComponent):\n    _w = pathlib.Path(\"/app/.venv/lib/python3.14/site-packages/<mod>.py\").write_text(payload)\n```\n\nThe planted module both writes a unique marker file and raises\n`RuntimeError(\"PLANTED_EXEC:<token>:\" + subprocess.check_output(\"id\", shell=True))`.\n`validate_code` only catches `ModuleNotFoundError`, so the `RuntimeError` propagates\nto the route handler, which returns HTTP 500 with `detail=str(e)` — exfiltrating the\ncommand output directly in the HTTP response.\n\nIn 1.12.3 the same request path performs\n`importlib.util.find_spec(alias.name.split(\".\")[0])` and never executes module code;\nthe identical attacker procedure therefore produces HTTP 200, empty errors, and no\nmarker file.\n\n- **Fix commit:** `461506ac2f38f70a994b5140572b876448c11e4c`\n  (`fix(security): close pathlib/io/codecs scanner bypass and stop validate_code from\n  executing imports`, PR #15201, H1-3992099 / LE-2683).\n- **Fixed release:** Langflow OSS 1.12.3 (git tag `v1.12.3` =\n  `fec71dca901949c09ed4d63315804337cd2eb13d`).\n\nNote on CVE mapping: the IBM bulletin for 1.12.3 lists 25 CVEs without per-CVE commit\nmapping. CVE-2026-93674 is the 9.8 PR:N CWE-94 (\"code injection / OS command\") entry;\nthe `validate_code` import-execution sink fixed by 461506ac2f is the matching\nunauthenticated remote code-execution fix in the 1.12.3 security train. The public\nthird-party PoC (rmhowe425/POC-CVE-2026-93674) targets the MCP stdio endpoint\n(`/api/v2/mcp/servers`), which corresponds to the earlier GHSA-w794-rj3p-xv45 /\nCVE-2026-105697 fix (1.10.3) — that allowlist is already present in 1.12.2, so the\nMCP path is not the 1.12.2→1.12.3 divergence; the validate/code path is.\n\n## Reproduction Steps\n\n1. `bundle/repro/reproduction_steps.sh` (self-contained; requires docker, curl, jq).\n2. The script:\n   - Pulls/pins the official images by digest: vulnerable\n     `langflowai/langflow@sha256:79c02794adebe82d756b7152ce4feebe4a5426e1faf3fe5b5d0dd08f304510c4`\n     (v1.12.2) and fixed\n     `langflowai/langflow@sha256:34055a07d446de51760e28dab6332e22624e5f48dca611567779992fc32c5ec0`\n     (v1.12.3).\n   - Starts **two fresh vulnerable containers** and **two fresh fixed containers**\n     (`LANGFLOW_AUTO_LOGIN=true`, the OSS package default), waiting for `/health`.\n   - Per attempt: (1) `GET /api/v1/auto_login` with no credentials → superuser JWT;\n     (2) `POST /api/v1/custom_component` plants `pruva_planted_<run>_<n>.py` into\n     site-packages via class-body `exec()`; (3) `POST /api/v1/validate/code`\n     `{\"code\":\"import <module>\"}` triggers the vulnerable import execution; the\n     marker file is read back out of the container.\n   - Vulnerable pass criteria: HTTP 500 + `PLANTED_EXEC:<token>:uid=1000(user)...`\n     in the response body + marker file containing the unique token inside the\n     container. Fixed pass criteria: HTTP 200, empty errors, no marker.\n3. Expected evidence: 2/2 vulnerable attempts execute attacker code; 2/2 fixed\n   attempts do not (identical procedure, plant still succeeds on fixed — proving the\n   divergence is exactly the validate/code import execution).\n\n## Evidence\n\n- Driver log: `bundle/logs/reproduction_steps.log`\n- Image identity: `bundle/logs/repro/image_identity.txt`\n- Per-attempt artifacts (`{vuln,fixed}_attempt_{1,2}_*` under\n  `bundle/logs/repro/attempts/`): auto-login token responses, plant\n  requests/responses, trigger requests/responses, planted module content, marker\n  files, container logs. All SHA-256-bound in\n  `bundle/repro/runtime_manifest.json`.\n- Key excerpt (vulnerable, both attempts):\n  `POST /api/v1/validate/code` → `HTTP=500`,\n  body `{\"detail\":\"PLANTED_EXEC:PRUVA-CVE-2026-93674-<run>-VULN-<n>:uid=1000(user) gid=0(root) groups=0(root)\"}`,\n  and `marker.txt` = `PRUVA-CVE-2026-93674-<run>-VULN-<n> uid=1000(user) gid=0(root) groups=0(root)`.\n- Key excerpt (fixed, both attempts): identical requests → `HTTP=200`,\n  body `{\"imports\":{\"errors\":[]},\"function\":{\"errors\":[]}}`, empty marker file.\n- Environment: Docker on Linux x86_64; images digest-pinned as above; Python 3.14\n  inside the container; `LANGFLOW_AUTO_LOGIN=true` (package-level default; the image\n  sets it to `false`, which only changes the bootstrap to credential-based login —\n  the validate/code sink itself is identical).\n\n## Recommendations / Next Steps\n\n- Upgrade to Langflow OSS **1.12.3** or later (IBM/vendor guidance; the fix replaces\n  `import_module()` with `find_spec()` in `validate_code`).\n- Until upgraded: do not expose Langflow to untrusted networks; set\n  `LANGFLOW_AUTO_LOGIN=false` and strong superuser credentials (raises the bar to\n  authenticated, but the sink still executes for any authenticated user on ≤1.12.2);\n  restrict file-system write access of the service account to site-packages.\n- Defense-in-depth: treat every \"validation\" endpoint as non-executing (audit for\n  other `import_module`/`exec` uses on request paths), and consider read-only root\n  filesystems / non-root containers.\n- Testing: regression test that `POST /api/v1/validate/code` with an import of a\n  planted module never executes it (covered upstream by the lfx-side tests added in\n  the fix commit).\n\n## Additional Notes\n\n- **Idempotency:** the script is fully idempotent — each attempt uses a fresh,\n  uniquely-named container and a unique module/marker name, and a `trap` removes all\n  containers on exit. Re-running produces fresh unique tokens/markers.\n- **Repeatability:** the exploit ran twice per side (two fresh vulnerable processes,\n  two fresh fixed processes) with identical outcomes.\n- **Why two stages:** the CVE sink (`validate_code` import execution) requires an\n  importable attacker module. The plant uses Langflow's built-in custom-component\n  code-evaluation feature, which behaves identically on 1.12.2 and 1.12.3 — the\n  vulnerable/fixed divergence is isolated entirely to the `validate_code` step,\n  which is what the 1.12.3 fix changed.\n- **Limitations:** none affecting the verdict. The reproduction uses the official\n  vendor images at the exact vulnerable/fixed digests; no sanitizers, mocks, or\n  instrumentation were used.\n","cve_id":"CVE-2026-93674","cwe_id":"CWE-94 Improper Control of Generation of Code (Code Injection)","source_url":"https://nvd.nist.gov/vuln/detail/CVE-2026-93674","package":{"name":"langflow-ai/langflow","ecosystem":"PyPI / Python","affected_versions":"1.0.0 through 1.12.2","fixed_version":"1.12.3"},"reproduced_at":"2026-10-09T06:21:33.361066+00:00","duration_secs":4474.0,"tool_calls":241,"handoffs":2,"total_cost_usd":6.857421,"agent_costs":{"claim_matcher":0.033317,"judge":0.425569,"learning_policy":0.013078,"repro":4.531539,"support":0.125129,"vuln_variant":1.728789},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.033317},"judge":{"gpt-5.6-sol":0.425569},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.013078},"repro":{"accounts/fireworks/models/kimi-k3":4.531539},"support":{"accounts/fireworks/models/kimi-k3":0.125129},"vuln_variant":{"accounts/fireworks/models/kimi-k3":1.728789}},"vulnerable_version_variant_outcome":"unknown","fix_bypass_outcome":"unknown","variant_disclosure_state":"unknown","quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-10-09T06:21:34.294952+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":10252,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":13366,"category":"reproduction_script"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_autologin_response.json","filename":"fixed_attempt_1_autologin_response.json","size":435,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_container.log","filename":"fixed_attempt_1_container.log","size":73292,"category":"log"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_marker.txt","filename":"fixed_attempt_1_marker.txt","size":0,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_plant_request.json","filename":"fixed_attempt_1_plant_request.json","size":720,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_plant_response.json","filename":"fixed_attempt_1_plant_response.json","size":1499,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_planted_module.py.txt","filename":"fixed_attempt_1_planted_module.py.txt","size":317,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_trigger_request.json","filename":"fixed_attempt_1_trigger_request.json","size":54,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_autologin_response.json","filename":"fixed_attempt_2_autologin_response.json","size":435,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_container.log","filename":"fixed_attempt_2_container.log","size":73292,"category":"log"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_marker.txt","filename":"fixed_attempt_2_marker.txt","size":0,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_plant_request.json","filename":"fixed_attempt_2_plant_request.json","size":720,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_plant_response.json","filename":"fixed_attempt_2_plant_response.json","size":1499,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_planted_module.py.txt","filename":"fixed_attempt_2_planted_module.py.txt","size":317,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_trigger_request.json","filename":"fixed_attempt_2_trigger_request.json","size":54,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_trigger_response.json","filename":"fixed_attempt_2_trigger_response.json","size":50,"category":"other"},{"path":"bundle/logs/repro/attempts/vuln_attempt_1_autologin_response.json","filename":"vuln_attempt_1_autologin_response.json","size":435,"category":"other"},{"path":"bundle/logs/repro/attempts/vuln_attempt_1_container.log","filename":"vuln_attempt_1_container.log","size":72887,"category":"log"},{"path":"bundle/logs/repro/attempts/vuln_attempt_1_planted_module.py.txt","filename":"vuln_attempt_1_planted_module.py.txt","size":315,"category":"other"},{"path":"bundle/logs/repro/attempts/vuln_attempt_2_autologin_response.json","filename":"vuln_attempt_2_autologin_response.json","size":435,"category":"other"},{"path":"bundle/logs/repro/attempts/vuln_attempt_2_container.log","filename":"vuln_attempt_2_container.log","size":72887,"category":"log"},{"path":"bundle/logs/repro/attempts/vuln_attempt_2_plant_request.json","filename":"vuln_attempt_2_plant_request.json","size":718,"category":"other"},{"path":"bundle/logs/repro/attempts/vuln_attempt_2_plant_response.json","filename":"vuln_attempt_2_plant_response.json","size":1497,"category":"other"},{"path":"bundle/logs/repro/attempts/vuln_attempt_2_planted_module.py.txt","filename":"vuln_attempt_2_planted_module.py.txt","size":315,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":3464,"category":"log"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":7516,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1677,"category":"other"}]}