Writeup
Bypassing LangFlow's Code-Execution Controls Via the MCP STDIO Path
Source: LangFlow任意 npm/PyPI 包代码执行漏洞 — 先知安全技术社区 (xz.aliyun.com), original language: Chinese. Translated & adapted by Sourav Banerjee.
Original URL: https://xz.aliyun.com/news/92807
LangFlow is a visual, low-code platform for building agentic AI workflows, and it has become a popular target for offensive research precisely because it exposes "code execution as a feature." This writeup, published by SecurityPaper on the Xianzhi (先知) security community, documents a subtle bypass: the admin-only authorization check for MCP STDIO servers lives only in the REST layer, and can be completely sidestepped through the flow-graph component parameter path, leading to arbitrary npm/PyPI package execution.
Vulnerability description
The authorization check for MCP STDIO server configuration (ensure_mcp_stdio_access) is deployed only at the REST layer. By going through the flow-graph component parameter path, an attacker fully bypasses it, allowing execution of arbitrary npm/PyPI packages on a hardened, effectively defaults-stripped LangFlow instance.
Environment setup
The author stood up a hardened non-development LangFlow environment, enabling public signup:
LANGFLOW_NEW_USER_IS_ACTIVE=true
LANGFLOW_ENABLE_SIGNUP=trueInstall LangFlow in a virtualenv:
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install langflowThen a hardened prod.env, notably closing the previously-known custom-component RCE:
LANGFLOW_AUTO_LOGIN=false
# Default true: GET /api/v1/auto_login returns a superuser JWT with zero credentials
LANGFLOW_SUPERUSER=admin
LANGFLOW_SUPERUSER_PASSWORD=Admin@123
LANGFLOW_NEW_USER_IS_ACTIVE=true
LANGFLOW_ENABLE_SIGNUP=true
LANGFLOW_WEBHOOK_AUTH_ENABLE=true
# Core hardening: only execute component code matching known template hashes.
# Directly closes LF-02 (user-submitted arbitrary Python compiled+execed server-side).
LANGFLOW_ALLOW_CUSTOM_COMPONENTS=false
# Second gate: component code editing is admin-only.
LANGFLOW_CUSTOM_COMPONENT_ADMIN_ONLY=true
# Built-in file components may only read paths inside the user storage dir.
# Closes LF-03's entry (reading ~/.cache/langflow/secret_key).
LANGFLOW_RESTRICT_LOCAL_FILE_ACCESS=true
# Global var resolution must not fall back to reading process env vars.
LANGFLOW_FALLBACK_TO_ENV_VAR=false
# Connector components cannot access loopback — compresses the SSRF surface.
LANGFLOW_CONNECTOR_SSRF_ALLOW_LOOPBACK=false
LANGFLOW_CORS_ORIGINS=http://192.168.102.154:7861Start it:
./.venv/bin/python -m langflow run --host 0.0.0.0 --port 7861 --no-dev --no-open-browser --env-file prod.envRoot cause
LangFlow implements an admin-only authorization check for MCP STDIO-type server config: ensure_mcp_stdio_access. When code-execution restriction policies are active (allow_custom_components=false or custom_component_admin_only=true), only superusers may configure STDIO servers.
Trying to configure a STDIO server with a freshly registered account returns "permission denied — administrators only."
In langflow/api/v2/mcp.py:42, the check has a positional flaw: all four of its call sites across the project live in the /api/v2/mcp/servers REST routes.
But MCP STDIO configuration enters the system through a second entry point: flow-graph node parameters. Via POST /api/v1/flows/ you create a flow carrying an MCPTools component with a crafted STDIO config, and POST /api/v1/run/{flow_id} triggers execution. The call chain update_tools → _connect_to_server → anyio.open_process spawns the process without any caller-identity check at the process-launch boundary.
Source comments show the developers were aware of the "inline flow config bypasses REST-layer validation" threat and tried to counter it — but validate_mcp_stdio_config only covers three dimensions: command, arguments, and environment variables. It does not cover caller identity.
The last check before the process is actually started also inspects only the command content, not who is calling. The process is spawned via the MCP SDK's anyio.open_process (POSIX) / create_windows_process (Windows).
Why ALLOW_CUSTOM_COMPONENTS=false doesn't save you
ALLOW_CUSTOM_COMPONENTS=false is designed so a user can't run their own code — only component code the server knows (official built-ins), verified by comparing a hash of the original component.
Creating a component is likewise rejected — administrators only.
However, the official MCPTools template source is visible to all logged-in users via GET /api/v1/all:
http://<host>:7861/api/v1/all
Once you have the original MCPTools code, the component code needs no modification — the malicious content all lives in the mcp_server parameter beside the code field, and parameters do not participate in the hash.
Additionally, although mcp_server_allowed_packages exists as a whitelist restriction, it is off by default. The whitelist only allows commands in the allowlist (with wrapper checks for things like bash), but since it's disabled by default, it provides no protection here.
Reproduction
- Register a user and log in to obtain a token (used in subsequent requests).
- Fetch the MCPTools template source:
GET /api/v1/all. - Create a flow carrying the malicious parameter — copy the template source verbatim into the
value, and useconcurrentlyto execute a command:"args": ["-y", "concurrently", "touch /tmp/langflow_success"] - The flow is created successfully (record the returned
id). - Issue the key / trigger the flow.
- Command executes successfully.
Takeaway
This is another strong data point for a recurring theme: authorization checks placed only in one layer (REST) are fragile when the same capability is reachable through another path (flow-graph component parameters). The fix is to enforce caller identity at the actual process-spawn boundary, not only at the HTTP routing layer.
As with all AI-development-surface research, the core lesson applies broadly: if your platform executes code or spawns processes as a feature, treat every path to that capability as an attack surface — and verify authorization at the point where the dangerous action actually happens.