一、背景:当"在画布上写 Python"成为产品的核心卖点

Langflow 是一款开源的可视化 AI 流程编排工具,用 Python 编写,后被 IBM 收编纳入其 AI 产品线。它的核心设计哲学可以用一句话概括:

"让用户在画布上写 Python,宿主机真的执行。"

这句话听起来理所当然,但它的潜台词非常危险——任何一个能触达"Python 执行节点"的请求,本质上都握有宿主机上 Python 解释器的完整能力。Langflow 把这种能力当作"功能"来售卖,而不是当作"攻击面"来防御。

项目信息 内容
受影响版本 1.0.0 - 1.9.3
修复版本 1.9.4
漏洞组件 PythonREPLComponent(UI 上显示为 "Python Interpreter")
CVSS 3.1 评分 10.0(AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
修复方式 升级到 1.9.4,无 Workarounds

CVE-2026-10561 之所以能拿到 CVSS 满分 10.0,不是因为它有多"复杂",而是因为它精准地踩中了两个独立的、各自看似"不算致命"的缺陷,而这两个缺陷叠加在一起,构成了从"互联网上的任意匿名用户"到"宿主机任意代码执行"的完整闭环。


二、漏洞组件:PythonREPLComponent

Langflow 的画布上有一个组件叫做 PythonREPLComponent,在 UI 中显示为 "Python Interpreter"。它的职责很直接:接收用户提交的一段 Python 代码,然后在服务端用 exec() 执行它。

为了"看起来安全",这个组件实现了基于白名单的 global_imports 机制,试图把用户代码可访问的模块限制在一个小范围内(默认仅 ["math"])。设计者的推理是:白名单只放 math → 没有 os/subprocess/socket → 因此"安全"。但这个推理链条的第一步就是错的。


三、根因机制:CPython exec() 的 builtins 自动注入

这是整个漏洞最精妙、也最容易被忽视的部分。IBM 安全公告对此给出了精确描述。

3.1 漏洞代码概念模型

# 漏洞代码概念模型
def get_globals(self):
    globals_ = {}
    for module_name in self.global_imports:  # default: ["math"]
        globals_[module_name] = __import__(module_name)
    # 关键缺陷:从未设置 globals_["builtins"] = {}
    return globals_

# CPython exec() 行为:globals 中没有 "builtins" 键时,
# 会自动插入完整的 builtins 模块
exec(user_code, get_globals())
# → import, open, eval, __import__ 全部可达

3.2 逐行剖析

get_globals() 方法创建空字典 globals_,遍历 self.global_imports 白名单(默认 ["math"])把每个模块塞进去,然后返回。关键在于:返回的字典里没有 "builtins" 这个键。这看起来无伤大雅——"我只是没放 builtins 而已",但问题恰恰出在这里。

3.3 CPython exec() 的隐藏行为

CPython 的 exec(source, globals) 在执行前会检查传入的 globals 字典中是否存在 "builtins" 键(定义在 Python/bltinmodule.c / Python/ceval.c):

if "builtins" not in globals:
    globals["builtins"] = <完整的 builtins 模块>

也就是说:如果你显式设置 globals["builtins"] = {},用户代码就没有任何内置函数可用;但如果你根本不设置这个键,CPython 会"贴心地"帮你补上完整的 builtins 模块。 这是一个"不作为即默许"的经典案例。

3.4 后果:白名单形同虚设

一旦完整的 builtins 模块被自动注入,以下内置函数无论白名单如何都变得可达:

内置函数 用途 危害
__import__ 动态导入任意模块 __import__('os') 直接拿到 os
import 语句 模块导入 import os; os.system(...)
open 文件读写 读取 /etc/passwd、写入 webshell
eval 表达式求值 二次逃逸沙箱
exec 嵌套执行 绕过任何外层限制
getattr / setattr 属性访问 反射逃逸

因此,所谓的 global_imports 白名单——无论你配置成 ["math"] 还是 ["math", "json"]——在 exec() 的自动注入面前都是一张废纸。攻击者只需一行:

__import__('os').system('id')

就能在宿主机上以 Langflow 进程的权限执行任意系统命令。

3.5 ASCII 流程图:builtins 注入全链路

┌─────────────────────────────────────────────────────────────────┐
│                    PythonREPLComponent 执行链路                   │
└─────────────────────────────────────────────────────────────────┘
  用户提交的 Python 代码
        │
        ▼
┌───────────────────────┐
│  get_globals()        │
│  globals_ = {}        │
│  for m in ["math"]:   │
│    globals_[m] = ...  │
│  # 未设置 builtins 键  │◄── 缺陷所在
│  return globals_      │
└───────────┬───────────┘
            │
            ▼
┌───────────────────────────────────────────────────┐
│  exec(user_code, globals_)                         │
│                                                    │
│  CPython 内部检查:                                  │
│    if "builtins" not in globals_:                  │
│        globals_["builtins"] = <完整 builtins 模块>  │◄── 自动注入
│                                                    │
│  现在 globals_ 包含:                                │
│    "math"     → math 模块 (白名单)                  │
│    "builtins" → 完整 builtins (自动注入!)           │
└───────────┬───────────────────────────────────────┘
            │
            ▼
┌───────────────────────────────────────────────────┐
│  用户代码执行环境                                    │
│                                                    │
│  __import__('os').system('...')   ← 可达!          │
│  open('/etc/passwd').read()        ← 可达!          │
│  eval(...)                         ← 可达!          │
│  import subprocess                 ← 可达!          │
└───────────────────────────────────────────────────┘
            │
            ▼
        宿主机 RCE (以 Langflow 进程权限运行)

四、认证绕过:auto_login 默认开启

builtins 注入是"枪",认证绕过则是"把枪递给了一个不需要任何身份的人"。

4.1 LANGFLOW_AUTO_LOGIN=true

Langflow 有一个配置项 LANGFLOW_AUTO_LOGIN,默认值为 true。当这个选项开启时,存在一个端点:

GET /api/v1/auto_login

这个端点不需要任何凭据,请求即可签发一个超级用户(superuser)的 JWT。拿到这个 JWT 之后,攻击者就拥有了 Langflow 的全部管理员权限,包括访问 PythonREPLComponent 相关的接口。

4.2 为什么这是默认配置

Langflow 大量被用于本地开发、原型验证、教学演示等场景,用户不想配置账号密码,只想打开浏览器就开始拖拽画布,于是 auto_login=true 成了默认值。但只要实例暴露在网络上(误配置、内网横向可达、公网部署),任何能访问端口的人都能一键拿到超级管理员权限。

4.3 认证绕过流程

攻击者 (匿名, 无凭据)
    │  GET /api/v1/auto_login (无认证头)
    ▼
Langflow 后端: LANGFLOW_AUTO_LOGIN == true ?
    │ yes → 签发 superuser JWT (无需密码)
    ▼
超级用户 JWT → 可访问所有 API → 可调用 PythonREPLComponent

五、两个独立失败的叠加:为什么是 CVSS 10.0

这是理解这个漏洞"满分"属性的关键。CVE-2026-10561 不是单一缺陷,而是两个独立失败的叠加

5.1 失败 #1:Python 执行隔离失败

PythonREPLComponent 的代码执行节点没有任何隔离机制

  • 没有命名空间隔离(与宿主进程共享同一个 Python 解释器实例)
  • 没有 seccomp 限制系统调用
  • 没有降权(以 Langflow 服务进程的权限运行,通常是高权限)
  • 没有 builtins 清空(即本漏洞的核心)

代码执行节点与 Langflow 服务跑在同一个执行环境中。这意味着:用户代码能做的事 = Langflow 进程能做的事。

5.2 失败 #2:认证绕过

auto_login=true 默认开启,/api/v1/auto_login 无凭据签发超级用户 JWT。

5.3 叠加效应

单独评估:
┌────────────────────────────────────────────────────────────┐
│  失败#1 单独 (隔离失败 + 认证完好)                          │
│  → 需要登录才能利用,但登录后可 RCE                          │
│  → CVSS ≈ 8.x (PR:L 或 PR:H 拉低分数)                      │
├────────────────────────────────────────────────────────────┤
│  失败#2 单独 (认证绕过 + 隔离完好)                          │
│  → 可拿到管理员,但管理员没有直接 RCE 的入口                  │
│  → CVSS ≈ 6-7 (主要是权限提升/信息泄露)                     │
├────────────────────────────────────────────────────────────┤
│  两者叠加 (认证绕过 + 隔离失败)                              │
│  → 未授权用户 → 超级管理员 → 无隔离 Python 执行 → RCE       │
│  → CVSS = 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)      │
└────────────────────────────────────────────────────────────┘

用 CVSS 3.1 的维度来拆解这个 10.0:

维度 含义
AV (Attack Vector) N (Network) 通过网络即可利用
AC (Attack Complexity) L (Low) 无需特殊条件
PR (Privileges Required) N (None) 无需任何认证
UI (User Interaction) N (None) 无需用户交互
S (Scope) C (Changed) 影响波及组件之外的系统
C (Confidentiality) H (High) 完全读取宿主机数据
I (Integrity) H (High) 完全篡改宿主机
A (Availability) H (High) 可致服务完全不可用

每一个维度都取到了最严重的值,这就是 10.0 满分的由来。关键是 PR:N——如果没有认证绕过,PR 至少是 L,分数会掉到 9.x 以下;如果没有隔离失败,C/I/A 不可能同时是 HS 也不可能是 C。两者缺一不可。


六、完整利用路径

将上面两个缺陷串联,完整攻击链路如下:

6.1 攻击链 ASCII 图

┌─────────────────────────────────────────────────────────────────────┐
│                     CVE-2026-10561 完整攻击链                        │
└─────────────────────────────────────────────────────────────────────┘

  [Step 1] 认证绕过
  ────────────────────────────────────────
  攻击者 ──GET /api/v1/auto_login──► Langflow
                                        │
                                        ▼
                                   签发 superuser JWT
                                        │
  [Step 2] 携带 JWT 访问 Python 执行接口  │
  ────────────────────────────────────────
  攻击者 ──POST /api/v1/... ────────► Langflow
            Authorization: Bearer <JWT>     │
            body: {"code": "..."}            │
                                             ▼
                                   PythonREPLComponent
                                   get_globals()
                                             │
  [Step 3] builtins 自动注入                 │
  ────────────────────────────────────────  │
                                   exec(code, globals_)
                                   globals_ 无 builtins 键
                                             │
                                             ▼
                                   CPython 自动注入
                                   完整 builtins 模块
                                             │
  [Step 4] RCE                              │
  ────────────────────────────────────────  │
                                   __import__('os').system('...')
                                             │
                                             ▼
                                   ┌──────────────────┐
                                   │  宿主机任意代码执行 │
                                   │  (Langflow 进程权限)│
                                   └──────────────────┘

6.2 PoC 代码

以下为概念验证(PoC)的脚本化描述,仅用于说明攻击原理:

import requests

TARGET = "http://target-langflow:7860"

# Step 1: 认证绕过 — auto_login 端点无需任何凭据
resp = requests.get(f"{TARGET}/api/v1/auto_login")
jwt_token = resp.json()["access_token"]
headers = {"Authorization": f"Bearer {jwt_token}"}

# Step 2: 构造恶意代码
# get_globals() 未设 builtins={} → CPython 自动注入完整 builtins
# → __import__ / open / eval 全部可达
malicious_code = """
import os
output = os.popen('id && hostname').read()       # 证明 RCE
with open('/etc/passwd') as f:                     # 读取宿主机文件
    print(f.read()[:200])
"""

# Step 3: 提交到 PythonREPLComponent 执行接口
resp = requests.post(f"{TARGET}/api/v1/...",
    json={"code": malicious_code}, headers=headers)
print(resp.text)
# 输出: uid=1000(langflow)... / langflow-server / root:x:0:0:root:...

6.3 两请求完成全链

整条链路不需要任何前置条件(无验证码、无速率限制、无交互),认证成本为零,压缩为两个 HTTP 请求即可完成从匿名到 RCE 的全流程。这就是标题"一行未授权请求干穿 CVSS 10"的由来。


七、架构级设计缺陷

CVE-2026-10561 不仅仅是一个"代码 bug",它暴露的是 Langflow 在架构层面的设计缺陷。

7.1 代码执行节点与宿主同环境

┌────────────────────────────────────────────────┐
│              Langflow 宿主进程                    │
│                                                  │
│  Web Server (FastAPI)  │  Flow Engine  │ 用户代码 │
│                                       exec()    │
│  ◄── 共享同一进程/解释器/权限/网络/文件系统 ──►   │
│                                                  │
│  ❌ 无命名空间隔离  ❌ 无 seccomp                 │
│  ❌ 无降权          ❌ 无 builtins 清空           │
└────────────────────────────────────────────────┘

用户提交的代码与 Langflow 自身服务逻辑跑在同一个进程里,能访问的内存、文件、网络、环境变量与 Langflow 完全相同。

7.2 RCE 是"功能"而非"漏洞"

这是最根本的问题。Langflow 的产品定位就是"让用户在画布上写 Python",因此"在服务端执行用户提供的 Python 代码"是产品的默认功能,而不是一个意外引入的漏洞。

这意味着:

  • 你不能简单地"禁用代码执行"来修复,因为那等于砍掉产品的核心功能;
  • 你必须在"允许执行"和"安全隔离"之间找到平衡,而 Langflow 在 1.9.4 之前完全没有做隔离;
  • 安全模型应该是:默认不信任用户代码,提供强隔离的执行环境(容器、沙箱、降权),而非依赖白名单这种可被绕过的软限制。

八、修复方案:升级到 1.9.4

IBM 安全公告明确指出:无 Workarounds(无缓解措施),唯一修复方式是升级到 1.9.4。

8.1 修复内容

1.9.4 版本同时修复了以下问题:

修复项 说明
builtins 注入 get_globals()显式设置 globals_["builtins"] = {},阻止 CPython 自动注入完整 builtins
认证绕过 修复 auto_login 相关的认证逻辑
CVE-2026-7664 同时修复 MCP transport endpoint 授权绕过(CVSS 9.8)

8.2 builtins 注入修复 diff(概念)

def get_globals(self):
    globals_ = {}
    for module_name in self.global_imports:
        globals_[module_name] = __import__(module_name)
+   globals_["builtins"] = {}   # 修复:显式清空,阻止 CPython 自动注入
    return globals_

关键改动只有一行:globals_["builtins"] = {}——显式告诉 CPython"我不需要 builtins",从而阻止自动注入。设置后 __import__openevalimport 语句全部失效,白名单机制才真正生效。

注意:仅清空 builtins 并不能提供真正的安全隔离(攻击者仍可通过类型子类化、object.__subclasses__() 等方式逃逸纯 Python 沙箱)。真正的修复还需配合进程级/容器级隔离。但在认证完好的前提下,这已将"未授权 RCE"降级为"授权用户的沙箱逃逸",攻击门槛和影响范围大幅降低。

8.3 认证绕过修复

对于 auto_login,1.9.4 对 /api/v1/auto_login 端点的认证逻辑进行了收紧。在生产环境部署时,强烈建议:

# 显式关闭 auto_login
export LANGFLOW_AUTO_LOGIN=false

# 配置真实的管理员账号
export LANGFLOW_SUPERUSER=admin
export LANGFLOW_SUPERUSER_PASSWORD=<强密码>

九、附加发现:Custom Component 的 SHA-256 截断碰撞

在分析 CVE-2026-10561 的过程中,还发现了一个与 Custom Component allow-list 相关的碰撞漏洞。

9.1 漏洞原理

Langflow 的 Custom Component 机制允许用户提交自定义组件代码。为了做 allow-list(允许列表)校验,系统对组件代码计算 SHA-256 哈希,但只取前 12 个十六进制字符(即 48 bit)作为标识:

# 概念代码
component_hash = hashlib.sha256(component_code.encode()).hexdigest()[:12]
# 12 个十六进制字符 = 48 bit

48 bit 的哈希空间 = 2^48 ≈ 2.8 × 10^14 种可能。虽然这个数字看起来很大,但在现代硬件上进行生日攻击(birthday attack)寻找碰撞是可行的。

9.2 碰撞验证

实测结果:

硬件: 326 核 CPU
耗时: 7.4 分钟
结果: 成功找到两个不同的组件代码,其 SHA-256[:12] 相同

这意味着攻击者可以构造一个与白名单中合法组件哈希相同的恶意组件,从而绕过 allow-list 校验,将恶意代码注入到允许列表中。

9.3 为什么 48 bit 不够

生日攻击所需尝试次数约为 2^(n/2) = 2^24 ≈ 1677 万次。以 326 核并行计算(约 3.26 × 10^8 次/秒),考虑内存/IO 开销后实际约 7.4 分钟即可碰撞成功。在 2026 年的硬件条件下,48 bit 截断完全不安全。安全的做法是使用完整的 SHA-256(256 bit),或至少截断到 128 bit 以上。


十、相关漏洞:CVE-2026-7664

在 1.9.4 版本中,与 CVE-2026-10561 一同修复的还有 CVE-2026-7664

项目 内容
CVE 编号 CVE-2026-7664
CVSS 评分 9.8
漏洞组件 MCP (Model Context Protocol) transport endpoint
漏洞类型 授权绕过

MCP transport endpoint 存在授权绕过问题,攻击者可绕过认证访问 MCP 相关接口。虽然其 CVSS 略低于 CVE-2026-10561(9.8 vs 10.0,差异在于影响范围 S 维度),但同样是严重的认证缺陷,应在升级时一并修复。

这两个 CVE 共同表明:Langflow 在 1.9.4 之前的认证体系存在系统性问题,而非个别疏漏。


十一、防御方案总结

11.1 立即行动

  1. 升级 Langflow 到 1.9.4 或更高版本(唯一官方修复方式)
  2. 设置 LANGFLOW_AUTO_LOGIN=false,配置真实管理员账号
  3. 确认实例不暴露在公网(限制到内网/VPN)
  4. 在反向代理层增加认证(Basic Auth / OAuth / mTLS)
  5. 审计日志:检查 /api/v1/auto_login 的历史访问记录

11.2 架构层防御(纵深防御)

即使升级到 1.9.4,也建议从架构层面增加纵深防御:

  1. 容器级隔离:将 Python 执行节点放到独立的、降权的容器中,与主服务隔离文件系统、网络命名空间、进程空间。
  2. seccomp + AppArmor/SELinux:限制可用系统调用,启用强制访问控制。
  3. 网络分段:执行容器不应有出站网络权限(或仅白名单域名),防止数据外传和反弹 shell。
  4. 资源限制:设置 CPU、内存、执行时间限制,防止资源耗尽。
  5. 审计与监控:记录所有代码执行请求的来源、时间、代码内容,建立异常检测规则。

11.3 部署配置加固

LANGFLOW_AUTO_LOGIN=false
LANGFLOW_SUPERUSER=<your_admin>
LANGFLOW_SUPERUSER_PASSWORD=<strong_random_password>
LANGFLOW_HOST=127.0.0.1   # 仅监听本地,由反向代理(nginx/caddy)增加认证层

11.4 关于 builtins 沙箱的正确认知

需要强调,仅靠 globals_["builtins"] = {} 并不能构建安全的 Python 沙箱。Python 沙箱逃逸技术已非常成熟,常见路径包括类型子类化(().__class__.__bases__[0].__subclasses__())、通过已加载模块引用获取 builtins、利用 gc/f-code 等机制逃逸。

因此,builtins = {} 只是修复了最易被利用的路径,真正的安全隔离必须依赖进程级/容器级/操作系统级隔离机制。所有提供"在线执行用户 Python 代码"功能的产品都应以此为戒:不要试图用语言层面的限制替代操作系统级别的隔离。


十二、总结

CVE-2026-10561 是一个教科书级别的"满分漏洞"案例,其价值不在于攻击技术的精巧,而在于它清晰地展示了几个安全工程的核心教训:

  1. 不作为即默许get_globals() 未设置 builtins 键,本意是"不放 builtins",但 CPython 的行为是"你不放我就给你全套"。理解运行时的隐式行为是安全编码的前提。

  2. 默认配置决定安全基线auto_login=true 作为默认值,把"需要管理员账号才能利用的 RCE"变成了"任何人都能利用的 RCE"。

  3. 两个独立失败的叠加:单独看,隔离失败和认证绕过各自都"不算最严重";叠加之后,从"互联网匿名用户"到"宿主机 RCE"之间没有任何阻隔。安全评估必须考虑多个缺陷的组合效应。

  4. 功能即攻击面:当"执行用户代码"是核心功能时,安全模型必须围绕"如何安全地提供这个功能"来设计,而非寄希望于白名单等软限制。

  5. 无 Workarounds 的严重性:IBM 明确声明此漏洞没有缓解措施,唯一修复方式是升级。所有运行 1.0.0-1.9.3 的实例在升级之前完全暴露——没有任何配置调整能替代版本升级。

修复行动非常简单:升级到 1.9.4,关闭 auto_login,限制网络暴露。


免责声明

本文所述技术内容仅用于安全研究、防御建设和教育目的。所有漏洞分析、PoC 代码和攻击路径描述均基于公开的安全公告(IBM Security Advisory)和已修复版本的反向分析,旨在帮助安全工程师、运维人员和开发者理解漏洞原理并采取相应防护措施。

读者不得将本文中的任何技术信息用于未经授权的系统测试、渗透攻击或其他可能违反法律法规的活动。未经授权访问、攻击计算机系统在大多数国家和地区属于刑事犯罪。

作者不对任何人基于本文内容所采取的任何行为承担责任。在使用本文涉及的任何技术之前,请确保你已获得目标系统的合法授权,并遵守当地法律法规。如果你发现 Langflow 实例存在受影响版本,请立即联系系统管理员或通过 IBM 官方渠道进行报告。

本文中涉及的 CVE 编号、版本号、CVSS 评分等技术细节,均以官方安全公告为准。如官方信息与本文章存在差异,以官方公告为准。