{"repro_id":"REPRO-2026-00342","version":6,"title":"Langflow contains an unauthenticated remote code execution vulnerability in the validate endpoint that can lead to arbitrary Python code execution as root.","repro_type":"security","status":"published","severity":"critical","cvss_score":9.8,"description":"ADVISORY (ZDI-26-034, ZDI-CAN-27322): Langflow contains an unauthenticated remote code execution vulnerability in the validate endpoint that can lead to arbitrary Python code execution as root. Severity critical, CVSS 9.8, CWE-94. Disclosed as 0-day after vendor non-response; credit Peter Girnus (@gothburz), William Gamazo Sanchez, Alfredo Oliveira (Trend Research).","root_cause":"# RCA Report — CVE-2026-0768 (Langflow unauthenticated RCE via /api/v1/validate/code)\n\n## Summary\n\nLangflow (<= 1.2.x, and partially thereafter) exposes `POST /api/v1/validate/code` without authentication. The endpoint calls `validate_code()` in `src/backend/base/langflow/utils/validate.py`, which `ast.parse()`s the attacker-supplied `code` field and then **`exec()`s each top-level `FunctionDef` node** after compiling it. Python evaluates default-argument expressions and decorators **at function-definition time**, so a function body that never runs still executes arbitrary expressions embedded in its default arguments or decorators. The endpoint's exception handler returns the resulting error text in the HTTP response (`detail.function.errors[0]`), giving a built-in exfiltration channel for command output — a fully non-blind, unauthenticated remote code execution.\n\n## Impact\n\n- **Package/component:** `langflow` (`langflow.utils.validate.validate_code`, reached from the `/api/v1/validate/code` FastAPI route).\n- **Affected versions:** Unauthenticated on langflow <= 1.2.x (verified on the official image `v1.1.1`). Version 1.3.0 (commit `faac4db`, PR #6911) added `get_current_active_user` auth to the route only — `exec(code_obj)` of user code survives, and `LANGFLOW_AUTO_LOGIN=true` (the default) auto-authenticates requests, so default 1.3.0+ deployments remain effectively unauthenticated. Any authenticated user retains RCE on 1.3.0+. No complete fix exists as of the disclosed range (survives through >= 1.8.0-rc per advisory).\n- **Risk level:** Critical (CVSS 9.8, CWE-94, ZDI-26-034 / ZDI-CAN-27322). Sibling CVE-2025-3248 (same endpoint, same root cause) is in CISA KEV.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Unauthenticated arbitrary Python code execution in the Langflow server process (advisory says \"as root\"; in the official container image the process runs as `uid=1000(user) gid=0(root)`, i.e. full container compromise).\n- **Reproduced impact from this run:** **Full parity — unauthenticated remote command execution.** Two independent fresh Langflow 1.1.1 containers executed attacker-selected shell commands (`id`) via the default-argument `exec()` vector; output `uid=1000(user) gid=0(root) groups=0(root)` was exfiltrated in the HTTP response JSON at `detail.function.errors[0]`; each instance also wrote a unique marker file inside the container filesystem via the executed command (`marker_output` evidence). A decorator-based vector (`@exec(...)`) triggered the identical sink, proving payload-shape agnosticism.\n- **Parity:** `full`.\n- **Not demonstrated:** Nothing of the claimed impact is missing. (Note: the advisory's \"as root\" phrasing maps to `gid=0(root)` container execution observed here; the server process itself runs as uid 1000 in the official image.)\n\n## Root Cause\n\n1. The route `POST /api/v1/validate/code` (langflow <= 1.2.x) has no authentication dependency.\n2. `validate_code()` runs `ast.parse(code)`, then for each top-level `FunctionDef` node:\n   `code_obj = compile(ast.Module(body=[node], type_ignores=[]), '<string>', 'exec'); exec(code_obj)`\n   — deliberately executing user code to \"validate\" it.\n3. Python evaluates **default-argument expressions and decorator expressions when the `def` statement executes**, not when the function is called. An attacker embeds `exec('raise Exception(__import__(\"subprocess\").check_output(\"id\", shell=True))')` in a default argument; it runs during `exec(code_obj)` inside the server process.\n4. The raised `Exception` text (containing the command output) is captured by the endpoint's error handling and returned to the attacker in `detail.function.errors[0]` — a response-side exfiltration channel making the RCE non-blind.\n5. **Partial fix:** langflow 1.3.0 (commit `faac4db`, PR #6911) added `get_current_active_user` to the route. Because `LANGFLOW_AUTO_LOGIN=true` is the default, requests are auto-authenticated and the same payload still executes on default 1.3.0 deployments (verified in this run). With `LANGFLOW_AUTO_LOGIN=false`, the same request is rejected with HTTP 403 `An API key must be passed as query or header` and no code executes.\n\n## Reproduction Steps\n\n1. Script: `bundle/repro/reproduction_steps.sh` (self-contained; run with `bash bundle/repro/reproduction_steps.sh`). Two consecutive successful runs confirmed idempotency (exit 0 both times).\n2. What it does:\n   - Pulls digest-pinned official images: vulnerable `langflowai/langflow@sha256:b56d4cfe...` (v1.1.1) and fixed `langflowai/langflow@sha256:8c124064...` (1.3.0).\n   - Starts four fresh containers: two vulnerable (`LANGFLOW_AUTO_LOGIN=true`), one fixed with `LANGFLOW_AUTO_LOGIN=false` (auth enforced), one fixed with defaults (auto-login).\n   - Waits for `GET /health` == `{\"status\":\"ok\"}` on each.\n   - **Vulnerable attempts (x2):** sends the exact contract payload `{\"code\": \"def exploit(cd=exec('raise Exception(__import__(\\\"subprocess\\\").check_output(\\\"id\\\", shell=True))')): pass\"}`; asserts HTTP 200 and `uid=` in `detail.function.errors[0]`.\n   - **Marker-backed execution (x2 fresh instances):** payload writes a unique marker file to `/tmp/pruva_marker.txt` inside each container via the executed command; asserts marker bytes match and `uid=` is exfiltrated.\n   - **Decorator variant:** `@exec(...)` payload; asserts command output in `function.errors[0]`.\n   - **Fixed negative control (x2):** same payloads against auth-enforced 1.3.0; asserts 401/403, no `uid=`, and no marker file created in the container.\n   - **Partial-fix documentation (control B):** same payload against default 1.3.0 (auto-login); observes it still executes (expected partial-fix behavior, not a reproduction failure).\n   - Writes `bundle/repro/runtime_manifest.json` with target identity (image digests) and SHA-256 of all proof artifacts; cleans up containers.\n3. Expected evidence: HTTP 200 responses containing `b'uid=1000(user) gid=0(root) groups=0(root)\\n'` at `detail.function.errors[0]`, marker files with exact attacker-chosen bytes, and 403 rejection on the auth-enforced fixed build.\n\n## Evidence\n\nAll artifacts under `bundle/` (paths relative to bundle root), SHA-256 map in `bundle/repro/runtime_manifest.json`:\n\n- `logs/reproduction_steps.log` — full run transcript (diagnostic).\n- `logs/repro/image_identity.txt` — immutable image digests/arch used.\n- `logs/repro/containers.txt` — live container listing.\n- `logs/repro/attempts/vuln_attempt_{1,2}_{request,response}.txt` — primary payload and responses:\n  `{\"imports\":{\"errors\":[]},\"function\":{\"errors\":[\"b'uid=1000(user) gid=0(root) groups=0(root)\\n'\"]}}`\n- `logs/repro/attempts/vuln_marker_{a,b}_{request,response}.txt`, `vuln_{a,b}_marker.txt` — marker-backed command execution on two fresh instances (markers `PRUVA-CMDEXEC-1788272103-6040-A` / `-B`, exact bytes verified).\n- `logs/repro/attempts/vuln_variant_decorator_{request,response}.txt` — decorator vector output `b'1000\\n'` (`id -u`).\n- `logs/repro/attempts/fixed_attempt_{1,2}_{request,response}.txt`, `negative_control_observation.json` — 403 `{\"detail\":\"An API key must be passed as query or header\"}`, marker absent.\n- `logs/repro/attempts/fixed_auto_attempt_1_{request,response}.txt` — 1.3.0 default deployment still executes (partial fix).\n\n**Environment:** Docker (overlay2) on linux/x86_64; official images `langflowai/langflow` v1.1.1 (digest `sha256:b56d4cfe18284e9fb2f1ec2d1bc9a29107a8c893397543e4937a55cda0136cd3`) and 1.3.0 (digest `sha256:8c124064a4410ceff7a7ffbee3aec393e3b9fb2e3e43a163b537074143a38ca5`); no sanitizers or instrumentation; plain HTTP via curl.\n\n**Exploit knowledge records (current run):** primitive `9199a628-4fa2-4c50-9517-54e12e959e07` (control_flow_influence — full attacker control of executed Python via the validate_code exec sink) and derived capability `c216aee7-41df-4f5f-b157-9fc7518764cf` (command_execution, generic_runtime attestation, two marker-backed process samples + negative control). ⚠️ Record `2a7dbd1c-4e51-4243-87b6-0e8bfb97fe28` in `bundle/repro/exploit_knowledge.json` was an accidental placeholder submission (\"address_disclosure\", title \"Placeholder\") and is **invalid — no address disclosure was observed in this run**; it must be ignored by downstream stages and is not part of any composition.\n\n## Recommendations / Next Steps\n\n- **Fix:** stop `exec()`ing user-supplied code during validation. Validate with `ast.parse` + a strict allowlist walk over the AST (rejecting `exec`/`eval`/`__import__`/dunder attribute access anywhere in the tree — defaults, decorators, lambdas, comprehensions — not just function bodies), or run validation in a sandboxed subprocess with no network/fs and resource limits. Auth alone is not a fix.\n- **Upgrade guidance:** no complete fix exists in the disclosed range; 1.3.0+ only adds route auth, defeated by default `LANGFLOW_AUTO_LOGIN=true`. Set `LANGFLOW_AUTO_LOGIN=false` + API-key auth to at least force authentication, and restrict `/api/v1/validate/code` at the reverse proxy/WAF.\n- **Detection:** requests to `/api/v1/validate/code` whose body contains default-arg/decorator `exec(`/`eval(`/`__import__(`/`subprocess` patterns; EDR: Langflow python worker spawning shell children.\n\n## Additional Notes\n\n- **Idempotency:** the script removes prior containers, reclaims ports 27860–27863, rewrites attempt files, and passed two consecutive clean runs (exit 0, all checks green each time).\n- **Limitations:** host ports are fixed (27860–27863) and must be free; the script checks and fails fast if occupied. First execution on a cold Docker cache pulls ~2 images (several minutes); subsequent runs reuse them. The 1.3.0 \"fixed\" image is a partial-fix control (route auth added), matching the advisory's affected/fix state — the exec sink itself remains exploitable post-auth, which is exactly what control B documents and what the vuln_variant stage can explore further.\n- Marker evidence files were extracted with `docker exec cat` (not `docker cp`) to preserve worker file ownership in the bundle.\n","cve_id":"CVE-2026-0768","cwe_id":"CWE-94","source_url":"https://cve.org/CVERecord?id=CVE-2026-0768","package":{"name":"langflow-ai/langflow","ecosystem":"PyPI","affected_versions":"Endpoint fully unauthenticated in langflow <= 1.2.x; exec(code_obj) of user code persists through >= 1.8.0-rc. NO COMPLETE FIX EXISTS.","fixed_version":"unknown"},"reproduced_at":"2026-09-02T03:58:13.831081+00:00","duration_secs":3788.0,"tool_calls":227,"handoffs":2,"total_cost_usd":3.521868,"agent_costs":{"claim_matcher":0.028534,"judge":0.499435,"learning_policy":0.019446,"repro":0.724979,"support":0.014803,"vuln_variant":2.234671},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.028534},"judge":{"gpt-5.6-sol":0.499435},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.019446},"repro":{"accounts/fireworks/models/glm-5p3":0.724979},"support":{"accounts/fireworks/models/glm-5p3":0.014803},"vuln_variant":{"accounts/fireworks/models/glm-5p3":2.234671}},"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-09-02T03:58:14.517592+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":10073,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":16107,"category":"reproduction_script"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_request.txt","filename":"fixed_attempt_1_request.txt","size":254,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_1_response.txt","filename":"fixed_attempt_1_response.txt","size":67,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_attempt_2_request.txt","filename":"fixed_attempt_2_request.txt","size":326,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_auto_attempt_1_request.txt","filename":"fixed_auto_attempt_1_request.txt","size":254,"category":"other"},{"path":"bundle/logs/repro/attempts/fixed_auto_attempt_1_response.txt","filename":"fixed_auto_attempt_1_response.txt","size":109,"category":"other"},{"path":"bundle/logs/repro/attempts/vuln_marker_b_request.txt","filename":"vuln_marker_b_request.txt","size":319,"category":"other"},{"path":"bundle/logs/repro/attempts/vuln_variant_decorator_request.txt","filename":"vuln_variant_decorator_request.txt","size":251,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":5090,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1649,"category":"other"}]}