# REPRO-2026-00381: IBM Langflow OSS 1.0.0 through 1.12.2 is vulnerable to remote unauthenticated code execution via OS command injection. ## Summary Status: published Severity: critical CVSS: 9.8 / 10 CWE: CWE-94 Improper Control of Generation of Code (Code Injection) (Improper Control of Generation of Code ('Code Injection')) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00381 CVE: CVE-2026-93674 ## Package Name: langflow-ai/langflow Ecosystem: PyPI / Python Affected: 1.0.0 through 1.12.2 Fixed: 1.12.3 ## Root Cause # CVE-2026-93674 — Root Cause Analysis ## Summary Langflow OSS through 1.12.2 exposes `POST /api/v1/validate/code`, a "validation-only" endpoint whose backend (`lfx.custom.validate.validate_code`) calls `importlib.import_module()` on **every import statement** found in attacker-supplied code. Importing a module executes its top-level code, so the endpoint runs arbitrary Python inside the Langflow server process during what is documented as a non-executing validation step. Combined with (a) the default single-user configuration (`LANGFLOW_AUTO_LOGIN=true`, which lets any network client mint a superuser token from `GET /api/v1/auto_login` with no credentials) and (b) a second remote code-evaluation sink (`POST /api/v1/custom_component`, which `exec()`s the attacker-supplied component class body and can plant a malicious module into the server-writable `site-packages`), a remote unauthenticated attacker obtains arbitrary OS command execution as the Langflow service user. The vulnerability is fixed in Langflow 1.12.3 by commit `461506ac2f38f70a994b5140572b876448c11e4c` ("fix(security): close pathlib/io/codecs scanner bypass and stop validate_code from executing imports", H1-3992099 / LE-2683), which replaces `import_module()` with `importlib.util.find_spec()` — locate-only, never executing module code. ## Impact - **Package/component:** `langflow` / `langflow-base` / `lfx` (PyPI), specifically `lfx.custom.validate.validate_code` behind the FastAPI route `POST /api/v1/validate/code`. - **Affected versions:** 1.0.0 through 1.12.2 (IBM bulletin / NVD CPE range; the vulnerable `importlib.import_module()` loop is present in v1.12.2 and removed in v1.12.3). - **Risk level:** Critical (CVSS 9.8, AV:N/AC:L/PR:N/UI:N). Full remote, unauthenticated OS command execution with the privileges of the Langflow service account (in the official container: `uid=1000(user) gid=0(root)`), i.e. complete compromise of flows, stored credentials/global variables, and any data readable by the service. ## Impact Parity - **Disclosed/claimed maximum impact:** remote (unauthenticated) arbitrary code / OS command execution (`code_execution`). - **Reproduced impact from this run:** remote unauthenticated OS command execution through the real HTTP API of the digest-pinned official `langflowai/langflow:1.12.2` image: the planted module ran `id` via `subprocess.check_output(..., shell=True)` inside the server process; the output (`uid=1000(user) gid=0(root) groups=0(root)`) was exfiltrated in-band in the HTTP 500 `detail` field of the very `POST /api/v1/validate/code` response, and a unique per-attempt marker file was written inside the container filesystem. - **Parity:** `full`. - **Not demonstrated:** nothing material — the claimed impact class was reproduced end-to-end. (A persistent shell/pivot was not attempted; it is not required for parity.) ## Root Cause `src/lfx/src/lfx/custom/validate.py` (v1.12.2), function `validate_code(code)`: ```python # Evaluate the import statements for node in tree.body: if isinstance(node, ast.Import): for alias in node.names: try: importlib.import_module(alias.name) # <-- EXECUTES module top-level code except ModuleNotFoundError as e: errors["imports"]["errors"].append(str(e)) ``` `importlib.import_module()` is not a lookup — it loads and **executes** the module. Because the endpoint is reachable by any network client under the default `LANGFLOW_AUTO_LOGIN=true` configuration (the auto-login route issues a superuser JWT without credentials), an attacker who can place a Python file on any `sys.path` entry writable by the service account gets it executed by simply sending `{"code": "import "}`. The official container runs as `uid=1000` and owns `/app/.venv/lib/python3.14/site-packages`, which is on `sys.path`, so the built-in custom-component code-evaluation feature (`POST /api/v1/custom_component` → `build_custom_component_template()` → `exec()` of the class body) provides the file-write primitive fully remotely: ```python class Planter(CustomComponent): _w = pathlib.Path("/app/.venv/lib/python3.14/site-packages/.py").write_text(payload) ``` The planted module both writes a unique marker file and raises `RuntimeError("PLANTED_EXEC::" + subprocess.check_output("id", shell=True))`. `validate_code` only catches `ModuleNotFoundError`, so the `RuntimeError` propagates to the route handler, which returns HTTP 500 with `detail=str(e)` — exfiltrating the command output directly in the HTTP response. In 1.12.3 the same request path performs `importlib.util.find_spec(alias.name.split(".")[0])` and never executes module code; the identical attacker procedure therefore produces HTTP 200, empty errors, and no marker file. - **Fix commit:** `461506ac2f38f70a994b5140572b876448c11e4c` (`fix(security): close pathlib/io/codecs scanner bypass and stop validate_code from executing imports`, PR #15201, H1-3992099 / LE-2683). - **Fixed release:** Langflow OSS 1.12.3 (git tag `v1.12.3` = `fec71dca901949c09ed4d63315804337cd2eb13d`). Note on CVE mapping: the IBM bulletin for 1.12.3 lists 25 CVEs without per-CVE commit mapping. CVE-2026-93674 is the 9.8 PR:N CWE-94 ("code injection / OS command") entry; the `validate_code` import-execution sink fixed by 461506ac2f is the matching unauthenticated remote code-execution fix in the 1.12.3 security train. The public third-party PoC (rmhowe425/POC-CVE-2026-93674) targets the MCP stdio endpoint (`/api/v2/mcp/servers`), which corresponds to the earlier GHSA-w794-rj3p-xv45 / CVE-2026-105697 fix (1.10.3) — that allowlist is already present in 1.12.2, so the MCP path is not the 1.12.2→1.12.3 divergence; the validate/code path is. ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (self-contained; requires docker, curl, jq). 2. The script: - Pulls/pins the official images by digest: vulnerable `langflowai/langflow@sha256:79c02794adebe82d756b7152ce4feebe4a5426e1faf3fe5b5d0dd08f304510c4` (v1.12.2) and fixed `langflowai/langflow@sha256:34055a07d446de51760e28dab6332e22624e5f48dca611567779992fc32c5ec0` (v1.12.3). - Starts **two fresh vulnerable containers** and **two fresh fixed containers** (`LANGFLOW_AUTO_LOGIN=true`, the OSS package default), waiting for `/health`. - Per attempt: (1) `GET /api/v1/auto_login` with no credentials → superuser JWT; (2) `POST /api/v1/custom_component` plants `pruva_planted__.py` into site-packages via class-body `exec()`; (3) `POST /api/v1/validate/code` `{"code":"import "}` triggers the vulnerable import execution; the marker file is read back out of the container. - Vulnerable pass criteria: HTTP 500 + `PLANTED_EXEC::uid=1000(user)...` in the response body + marker file containing the unique token inside the container. Fixed pass criteria: HTTP 200, empty errors, no marker. 3. Expected evidence: 2/2 vulnerable attempts execute attacker code; 2/2 fixed attempts do not (identical procedure, plant still succeeds on fixed — proving the divergence is exactly the validate/code import execution). ## Evidence - Driver log: `bundle/logs/reproduction_steps.log` - Image identity: `bundle/logs/repro/image_identity.txt` - Per-attempt artifacts (`{vuln,fixed}_attempt_{1,2}_*` under `bundle/logs/repro/attempts/`): auto-login token responses, plant requests/responses, trigger requests/responses, planted module content, marker files, container logs. All SHA-256-bound in `bundle/repro/runtime_manifest.json`. - Key excerpt (vulnerable, both attempts): `POST /api/v1/validate/code` → `HTTP=500`, body `{"detail":"PLANTED_EXEC:PRUVA-CVE-2026-93674--VULN-:uid=1000(user) gid=0(root) groups=0(root)"}`, and `marker.txt` = `PRUVA-CVE-2026-93674--VULN- uid=1000(user) gid=0(root) groups=0(root)`. - Key excerpt (fixed, both attempts): identical requests → `HTTP=200`, body `{"imports":{"errors":[]},"function":{"errors":[]}}`, empty marker file. - Environment: Docker on Linux x86_64; images digest-pinned as above; Python 3.14 inside the container; `LANGFLOW_AUTO_LOGIN=true` (package-level default; the image sets it to `false`, which only changes the bootstrap to credential-based login — the validate/code sink itself is identical). ## Recommendations / Next Steps - Upgrade to Langflow OSS **1.12.3** or later (IBM/vendor guidance; the fix replaces `import_module()` with `find_spec()` in `validate_code`). - Until upgraded: do not expose Langflow to untrusted networks; set `LANGFLOW_AUTO_LOGIN=false` and strong superuser credentials (raises the bar to authenticated, but the sink still executes for any authenticated user on ≤1.12.2); restrict file-system write access of the service account to site-packages. - Defense-in-depth: treat every "validation" endpoint as non-executing (audit for other `import_module`/`exec` uses on request paths), and consider read-only root filesystems / non-root containers. - Testing: regression test that `POST /api/v1/validate/code` with an import of a planted module never executes it (covered upstream by the lfx-side tests added in the fix commit). ## Additional Notes - **Idempotency:** the script is fully idempotent — each attempt uses a fresh, uniquely-named container and a unique module/marker name, and a `trap` removes all containers on exit. Re-running produces fresh unique tokens/markers. - **Repeatability:** the exploit ran twice per side (two fresh vulnerable processes, two fresh fixed processes) with identical outcomes. - **Why two stages:** the CVE sink (`validate_code` import execution) requires an importable attacker module. The plant uses Langflow's built-in custom-component code-evaluation feature, which behaves identically on 1.12.2 and 1.12.3 — the vulnerable/fixed divergence is isolated entirely to the `validate_code` step, which is what the 1.12.3 fix changed. - **Limitations:** none affecting the verdict. The reproduction uses the official vendor images at the exact vulnerable/fixed digests; no sanitizers, mocks, or instrumentation were used. ## Reproduction Details Reproduced: 2026-10-09T06:21:33.361Z Duration: 4474 seconds Tool calls: 241 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00381 pruva-verify CVE-2026-93674 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00381&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00381/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-93674 - Source: https://nvd.nist.gov/vuln/detail/CVE-2026-93674 ## Artifacts - bundle/repro/rca_report.md (analysis, 10252 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 13366 bytes) - bundle/logs/repro/attempts/fixed_attempt_1_autologin_response.json (other, 435 bytes) - bundle/logs/repro/attempts/fixed_attempt_1_container.log (log, 73292 bytes) - bundle/logs/repro/attempts/fixed_attempt_1_marker.txt (other, 0 bytes) - bundle/logs/repro/attempts/fixed_attempt_1_plant_request.json (other, 720 bytes) - bundle/logs/repro/attempts/fixed_attempt_1_plant_response.json (other, 1499 bytes) - bundle/logs/repro/attempts/fixed_attempt_1_planted_module.py.txt (other, 317 bytes) - bundle/logs/repro/attempts/fixed_attempt_1_trigger_request.json (other, 54 bytes) - bundle/logs/repro/attempts/fixed_attempt_2_autologin_response.json (other, 435 bytes) - bundle/logs/repro/attempts/fixed_attempt_2_container.log (log, 73292 bytes) - bundle/logs/repro/attempts/fixed_attempt_2_marker.txt (other, 0 bytes) - bundle/logs/repro/attempts/fixed_attempt_2_plant_request.json (other, 720 bytes) - bundle/logs/repro/attempts/fixed_attempt_2_plant_response.json (other, 1499 bytes) - bundle/logs/repro/attempts/fixed_attempt_2_planted_module.py.txt (other, 317 bytes) - bundle/logs/repro/attempts/fixed_attempt_2_trigger_request.json (other, 54 bytes) - bundle/logs/repro/attempts/fixed_attempt_2_trigger_response.json (other, 50 bytes) - bundle/logs/repro/attempts/vuln_attempt_1_autologin_response.json (other, 435 bytes) - bundle/logs/repro/attempts/vuln_attempt_1_container.log (log, 72887 bytes) - bundle/logs/repro/attempts/vuln_attempt_1_planted_module.py.txt (other, 315 bytes) - bundle/logs/repro/attempts/vuln_attempt_2_autologin_response.json (other, 435 bytes) - bundle/logs/repro/attempts/vuln_attempt_2_container.log (log, 72887 bytes) - bundle/logs/repro/attempts/vuln_attempt_2_plant_request.json (other, 718 bytes) - bundle/logs/repro/attempts/vuln_attempt_2_plant_response.json (other, 1497 bytes) - bundle/logs/repro/attempts/vuln_attempt_2_planted_module.py.txt (other, 315 bytes) - bundle/logs/reproduction_steps.log (log, 3464 bytes) - bundle/repro/runtime_manifest.json (other, 7516 bytes) - bundle/repro/validation_verdict.json (other, 1677 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00381 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00381/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00381 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev