CVE-2026-45727:CloakBrowser `cloakserve` 中由 `fingerprint` 引出的路径穿越与目录删除

在审计 CloakBrowser 的 cloakserve 功能时,我发现了一个很典型、也很容易被忽略的安全问题:一个看起来只是"浏览器指纹 seed"的参数,最终被当成文件系统路径的一部分使用,并在异常清理流程里进入了递归删除逻辑。

这个问题对应 CloakBrowser GitHub Issue #217:Unauthenticated cloakserve CDP endpoint allows path traversal leading to arbitrary directory deletion。漏洞确认影响 cloakbrowser 0.3.27,修复提交为 babef04

说明:本文按 CVE-2026-45727 记录该漏洞。发布前建议再次确认 CVE/NVD/GHSA 的官方状态,以官方条目为最终准绳。

背景:cloakserve 是什么

cloakserve 是 CloakBrowser 提供的 CDP multiplexer。它允许客户端通过同一个入口连接不同的浏览器实例,并通过 fingerprint 参数指定不同的浏览器身份。

一个典型连接形式大概是:

text 复制代码
http://host:9222/json/version?fingerprint=12345

从功能设计上看,fingerprint=12345 会被当成一个 seed。相同 seed 可以复用相同浏览器身份,不同 seed 则对应不同的 Chrome 进程和 profile 目录。

问题也正出在这里:这个 seed 不只是业务标识,它还流入了本地 profile 路径。

漏洞入口

漏洞入口是 fingerprint 查询参数。

公开 issue 中描述的关键数据流如下:

  1. parse_connection_params() 解析查询字符串;
  2. fingerprint 被复制到 result["seed"]
  3. ChromePool.get_or_launch() 将 seed 赋值给 seed_key
  4. 服务端拼接 user_data_dir = os.path.join(self._data_dir, seed_key)
  5. Chrome 启动参数中带上 --user-data-dir=<user_data_dir>
  6. 启动失败或进程清理时调用 shutil.rmtree(proc.user_data_dir, True)

这里缺少一个关键校验:服务端没有确认最终解析后的 user_data_dir 仍然位于预期的 data_dir 内。

于是,攻击者可控的 fingerprint 就不再只是一个 seed,而变成了路径组件。

从 seed 到路径穿越

如果传入正常 seed:

text 复制代码
fingerprint=12345

服务端期望得到的 profile 路径类似:

text 复制代码
/data/cloakserve/profiles/12345

但如果传入路径穿越 payload:

text 复制代码
fingerprint=../victim-delete-me

拼接后的路径会变成:

text 复制代码
/data/cloakserve/profiles/../victim-delete-me

解析后实际指向:

text 复制代码
/data/cloakserve/victim-delete-me

也就是说,目录已经逃出了 profiles 目录。

真正危险的点不只是"Chrome 使用了一个不该使用的 profile 路径",而是后续清理逻辑会对这个路径执行递归删除。

实验室复现

为了避免对真实环境造成破坏,我使用的是路由级复现:保留真实的 handle_json_version()ChromePool.get_or_launch() 逻辑,只 stub Chrome 进程启动和 CDP readiness。这样可以验证真实代码路径,又不会真的下载或运行 Chromium。

请求如下:

http 复制代码
GET /json/version?fingerprint=..%2Fvictim-delete-me HTTP/1.1
Host: 127.0.0.1:9222

观察到的结果:

text 复制代码
HTTP status: 502
Response: {"error": "Chrome failed to start"}

Configured data_dir:
<lab>/cloakserve-path-traversal/profiles

Resolved user_data_dir:
<lab>/cloakserve-path-traversal/victim-delete-me

Escaped data_dir: true
Victim sentinel existed before request: true
Victim directory existed after request: false
Deleted by cleanup: true

捕获到的 Chrome 参数尾部类似:

text 复制代码
--remote-debugging-port=5100
--remote-debugging-address=127.0.0.1
--user-data-dir=<lab>/cloakserve-path-traversal/profiles/../victim-delete-me

这里有一个很有意思的细节:请求最终返回的是 502,看起来像是"浏览器启动失败"。但安全影响已经发生了,因为失败分支触发了清理逻辑,而清理目标正是攻击者通过路径穿越控制后的目录。

这类漏洞非常容易被低估,因为表面现象是失败响应,而不是成功响应。

额外暴露面:未认证 CDP 元数据

除了目录删除,issue 中还记录了另一个相关风险:同一个未认证入口会返回 CDP webSocketDebuggerUrl 元数据。

测试请求:

http 复制代码
GET /json/version?fingerprint=12345 HTTP/1.1
Host: 127.0.0.1:9222

观察结果:

text 复制代码
HTTP status: 200
No auth header/token/cookie supplied: true
Response includes webSocketDebuggerUrl: true

这意味着如果 cloakserve 被部署为网络可达服务,攻击者不仅可以触发路径穿越相关逻辑,还可能获得 CDP WebSocket 地址。在暴露部署中,CDP 本身通常就是高危攻击面,因为它可以控制浏览器行为。

影响评估

该漏洞的实际影响取决于部署方式。

如果 cloakserve 只监听本机回环地址,并且没有被反向代理或端口映射暴露给不可信网络,那么攻击面相对有限。

但如果它在 Docker、服务器或云环境中被暴露为网络服务,攻击者可能做到:

text 复制代码
1. 无需认证访问 /json/version 等 CDP 辅助接口;
2. 通过 fingerprint 参数构造路径穿越;
3. 让 user_data_dir 逃出预期 data_dir;
4. 触发启动失败或清理流程;
5. 删除 cloakserve 进程用户有权限删除的目录;
6. 枚举或连接 CDP 元数据暴露出的调试入口;
7. 使用大量唯一 fingerprint 创建浏览器进程,造成资源耗尽。

公开 issue 中给出的建议评分是:

text 复制代码
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8

我个人更倾向于把它描述为"网络可达部署下可达 Critical,默认本地部署下风险下降"。因为它是否能被远程利用,强依赖 cloakserve 的监听地址、容器端口映射和外部访问控制。

根因分析

根因其实很朴素:将用户输入直接当成路径组件使用。

fingerprint 在业务语义里是一个 seed,但在实现语义里变成了目录名。只要一个外部输入进入文件路径,就必须回答三个问题:

text 复制代码
它是否允许路径分隔符?
它是否允许 .. 或绝对路径?
最终 resolve 后是否还在允许的根目录里?

旧实现没有完成这些约束。

而且,删除逻辑使用的是 shutil.rmtree(..., True)。第二个参数会忽略错误,这在普通清理流程里很方便,但在安全场景里会降低可见性:即使删错了目录,调用方也未必能立即看到异常。

修复分析

修复提交 babef04 做了几件关键事情。

第一,增加 seed 格式白名单:

python 复制代码
SAFE_SEED_RE = re.compile(r"^[A-Za-z0-9_-]{1,128}$")
RESERVED_SEEDS = {"__default__"}

这一步直接切断了 /\.、空字节、绝对路径、超长字符串等危险输入。

第二,在处理 query 参数 seed 时拒绝非法值:

python 复制代码
if not SAFE_SEED_RE.match(seed) or seed in RESERVED_SEEDS:
    raise web.HTTPBadRequest(
        text=json.dumps({"error": "Invalid fingerprint seed"}),
        content_type="application/json",
    )

第三,新增安全删除函数。删除前先 resolve 路径,再确认目标位于 data_dir 内,并且不能是 data_dir 本身:

python 复制代码
def _safe_rmtree(self, path: str) -> None:
    resolved = Path(path).resolve()
    data_resolved = Path(self._data_dir).resolve()
    if resolved == data_resolved or not resolved.is_relative_to(data_resolved):
        logger.error("Refusing to delete path outside data_dir: %s", resolved)
        return
    shutil.rmtree(path, True)

第四,裸机默认监听从 0.0.0.0 收紧为 127.0.0.1,容器环境下才使用 0.0.0.0

python 复制代码
in_container = os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv")
host = "0.0.0.0" if in_container else "127.0.0.1"
web.run_app(app, host=host, port=port, print=None)

这不是路径穿越本身的修复,但能有效降低"开发者在裸机上无意暴露 CDP 服务"的风险。

回归测试

修复还加入了针对性测试,覆盖两类重点。

第一类是 seed 校验:

text 复制代码
应拒绝:
../foo
../../etc
/etc/passwd
..
.
foo/bar
foo\bar
空字符串
超长 seed
__default__

同时允许正常 seed:

text 复制代码
12345
my-seed_01
ABC
test-seed

第二类是删除边界:

text 复制代码
拒绝删除 data_dir 外部目录;
拒绝删除 data_dir 本身;
允许删除 data_dir 内的合法 seed 子目录;
拒绝 data_dir/../victim 形式的穿越路径。

这组测试比较关键,因为单纯校验 seed 只能挡住当前入口;而 _safe_rmtree() 的边界检查则是最后一道防线。即使未来又出现新的路径来源,删除函数本身也不会轻易越界。

结语

CVE-2026-45727 这类漏洞不花哨,但很有代表性。它不是复杂内存破坏,也不是高深协议绕过,而是一个参数在不同语义层之间悄悄变了身份:从浏览器指纹 seed,变成 profile key,再变成文件路径,最后进入递归删除。

很多真实漏洞就是这样来的。

安全审计里最值得盯的,往往不是代码里写着"危险"的地方,而是那些看起来只是"顺手复用一下"的地方。

相关推荐
财经资讯数据_灵砚智能4 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年5月17日
大数据·人工智能·python·信息可视化·自然语言处理
Bacon4 小时前
装上就回不去了:CodeGraph 让 AI 编程效率飙升 92%,它到底做了什么?
前端·人工智能·后端
XD7429716364 小时前
科技早报|2026年5月18日:AI 平台开始补生产级控制面
人工智能·科技·云基础设施·科技新闻·开发者工具·自托管·科技早报
前端小超人rui4 小时前
Prompt 提示词原理/组成/编写原则/编写技巧
人工智能·大模型·prompt
前端不太难4 小时前
AI 不只是聊天框:鸿蒙 App 新入口
人工智能·状态模式·harmonyos
神州数码云基地4 小时前
拆解ComfyUI:如何用“节点化”思想重构生成式AI工作流?
人工智能·重构·智能体
摄影图4 小时前
科技企业研发宣传图片素材 适配多场景宣传使用需求
大数据·人工智能·科技·aigc·贴图·插画
郑寿昌4 小时前
SubQ颠覆Transformer:亚二次稀疏注意力革命
人工智能·深度学习·transformer
七牛开发者4 小时前
AI Coding Agent 如何工程化:从上下文污染到多 Agent 分工
人工智能