导语:Docker 沙箱逃逸事件(CVE-2026-103916)再次证明了一条朴素的安全定律——沙箱的强度等于它最薄弱的那条出入口。Docker Sandboxes 用 microVM 把 Claude Code、Codex、Gemini CLI 这类编码智能体关进了独立内核,allowlist 机制本身没有失守,失守的是它旁边那条从未被安全模型纳入视野的
sbx cp文件通道。本文以资深架构师视角,完整拆解这条侧信道的攻击链、修复为何需要两次、以及一条真正尊重边界的文件通道在代码层应该长什么样。
CVE-2026-103916 的 CVSS 3.1 评分为 8.2(High)。若你正在使用 Docker Sandboxes 承载 AI 编码智能体,请立即升级至 0.51.0。在完成升级之前,任何跨工作区边界的 sbx cp 操作都应被视为潜在的越权写入。
一、Docker 沙箱逃逸的攻击面全景:主通道守住了,侧信道失守了
要理解这次 Docker 沙箱逃逸为什么成立,必须先理解 Docker Sandboxes 的安全模型设计意图。它把智能体放进一台拥有独立内核、独立文件系统、独立网络的 microVM 中,宿主只把用户显式授权的目录(通常是项目工作区)共享进 VM。这些被授权目录构成一份 allowlist,是智能体理论上唯一能够触达的宿主区域。
这套设计在「共享文件夹」这条主通道上是成立的:路径校验、权限检查、测试覆盖都围绕它展开。问题出在架构师最容易忽略的地方——所有代表操作者(operator)执行的文件搬运动作,都会天然继承操作者的信任级别。复制、导出、下载、制品上传,这些动作在安全模型里被默认为「用户自己的行为」,因此不经过 allowlist 校验。而当被搬运的路径由智能体控制时,这份信任就被花在了智能体想指向的任何地方。
企业级架构规范
flowchart TD
subgraph 沙箱层 ["隔离执行层 (microVM Sandbox Tier)"]
A["AI Agent (Claude Code / Codex / Gemini CLI)"] --> B["工作区读写权限 (Workspace RW)"]
B --> C["植入符号链接 (Symlink Plant)"]
end
subgraph 通道层 ["文件搬运侧信道 (Side Channel Tier)"]
C --> D["sbx cp 命令 (Operator 权限执行)"]
D --> E["路径解析 (resolve symlinks)"]
E --> F["-L 校验仅覆盖末位 (Last Position Only)"]
end
subgraph 宿主层 ["宿主文件系统 (Host Filesystem Tier)"]
F --> G["越界写入 / 越界读取 (Out-of-Bounds Access)"]
G --> H[("工作区外敏感文件")]
end
subgraph 修复层 ["边界加固 (Boundary Enforcement Tier)"]
I["realpath 逐级解析父目录"] --> J["O_NOFOLLOW 拒绝末位链接"]
J --> K["openat2 RESOLVE_BENEATH 内核级约束"]
end
二、CVE-2026-103916 攻击链拆解:符号链接的四个位置,三个是敞开的
2.1 攻击前提:工作区被授予读写权限
Endor Labs 的 Peyton Kennedy 在 11 月 3 日公开了 CVE-2026-103916。攻击成立的前提极其普通:工作区对智能体是可读可写的。这意味着智能体可以在工作区内任意创建、替换目录与文件——包括把一个普通目录替换成一个指向宿主其他位置的符号链接。
关键细节在于:这个链接在 VM 内部指向「空」,因此单独植入它,智能体什么也得不到。它是一枚延时引信,只有当操作者执行一次「恰好经过该目录」的普通复制操作时才会引爆。Kennedy 演示了双向利用:
- 复制出沙箱(copy out):覆盖工作区之外的宿主文件,实现越权写入;
- 复制入沙箱(copy in):把宿主敏感文件拉进沙箱,实现越权读取。
2.2 根因:-L 标志只检查了路径的最后一个位置
命令并非毫无防护,它带有 -L 标志用于检查符号链接。但问题在于,一个路径上的符号链接可以出现在四个位置:路径中间的某个目录、路径末端的文件名、以及解析过程中的多级跳转。Kennedy 的测试表格给出了残酷结论——「四个位置中有三个是敞开的」。-L 只守住了末位,中间目录被替换成链接时,校验完全失效。
「只校验路径最后一段」是路径安全领域最经典的反模式。任何基于 path.basename 或字符串后缀匹配的边界检查,都无法防御中间目录被替换为符号链接的攻击。正确做法是对解析后的父目录做前缀归属校验。
2.3 修复为何需要两次:第一次补丁不完整
Endor Labs 记录显示,Docker 最初计划在 10 月 13 日的 0.48.0 版本修复,但「初始补丁上线后,Docker 团队判定修复不完整」。最终在 11 月 3 日的 0.51.0 版本发布说明中确认了完整修复:sbx cp 阻止挂载目录中的符号链接逃逸到宿主其他路径。
更值得警惕的是,这已经是 sbx cp 今年第二次逃逸。此前的 CVE-2026-17106 在 0.38.0 中修复,它检查的是被复制归档内部的路径,而这一次的缺陷在于——它没有检查复制动作起点与落点本身。两次漏洞,同一个命令,不同的检查盲区。
| 漏洞编号 | 受影响组件 | 检查盲区 | 修复版本 | CVSS |
|---|---|---|---|---|
| CVE-2026-17106 | sbx cp 归档解包 | 归档内部路径未校验 | 0.38.0 | — |
| CVE-2026-103916 | sbx cp 路径解析 | 起点/落点符号链接未校验 | 0.51.0 | 8.2 High |
三、为什么侧信道总被漏掉:沙箱安全模型的系统性盲区
Kennedy 自己的总结最值得被写进架构评审清单:「隔离原语本身是有效的,失败的是通往宿主的第二条路径,而安全模型没有把它计算在内。」他在今年早些时候于另一个智能体沙箱 brig 中发现了完全相同的形态。
原因非常「普通」,普通到几乎每个团队都会踩:
- 沙箱围绕主通道(共享文件夹)设计,allowlist 与测试都给了主通道;
- 代表操作者搬运文件的侧信道(copy、export、download、artifact upload)看起来像操作者自己的动作,于是继承了操作者的信任;
- 当智能体控制了路径,这份信任就被花在智能体指向的任何地方。
这不是 Docker 独有的问题,而是一类架构缺陷。任何让智能体在真实机器上产出文件的系统都存在这些侧信道:收集构建产物的构建步骤、保存附件的工具、把报告同步到共享存储的定时任务。每一个都需要在触碰磁盘的那一刻重新校验边界。
安全边界不应绑定在「通道」上,而应绑定在「系统调用」上。只要一次文件写入发生在宿主内核,边界就必须在该次 open 上被强制,而不是在调用它的 CLI 参数解析阶段被「约定」。
四、工程落地:一条真正尊重边界的文件通道该怎么写
4.1 Node 侧参考实现:解析父目录 + O_NOFOLLOW
下面是一份宿主侧「向智能体工作区内写入」的参考实现(非 Docker 官方代码,为架构示范)。它修复了三个关键点:校验发生在解析后的父目录上(正是 -L 留下的缺口);最终 open 使用 O_NOFOLLOW 拒绝末位链接;函数返回真实路径供调用方打印。
import { constants, promises as fs } from "node:fs";
import path from "node:path";
// 仅允许写入工作区内的路径,否则拒绝
export async function writeInside(
workspace: string,
relPath: string,
data: Uint8Array,
): Promise {
const root = await fs.realpath(workspace);
const target = path.resolve(root, relPath);
// 逐级解析父目录,而不是只检查文件名
const dir = await fs.realpath(path.dirname(target));
if (dir !== root && !dir.startsWith(root + path.sep)) {
throw new Error(`refused: ${relPath} resolves to ${dir}`);
}
const file = path.join(dir, path.basename(target));
// O_NOFOLLOW:若文件本身是链接,直接失败
const flags =
constants.O_WRONLY |
constants.O_CREAT |
constants.O_TRUNC |
constants.O_NOFOLLOW;
const fh = await fs.open(file, flags, 0o644);
try {
await fh.writeFile(data);
} finally {
await fh.close();
}
return file; // 返回真实路径,让人看得见落点
}
注意第三个设计细节:函数返回真实路径。Kennedy 特别指出,一次成功的 sbx cp 什么也不打印,因此旁边盯着屏幕的人根本无从察觉异常。可观测性本身就是安全边界的一部分。
4.2 从「检查后写入」到「内核强制」:openat2 与 securejoin
上面的实现仍有 TOCTOU 窗口:路径检查与随后的写入是两个步骤,如果智能体在复制执行期间重写工作区,就能在两步之间把目录换成链接。真正的工程级方案必须把边界下沉到内核。
- Linux:使用
openat2配合RESOLVE_BENEATH,让内核在open本身执行期间强制约束路径不得越出根目录; - Go 生态:可使用
cyphar/filepath-securejoin,Kennedy 指出该库其实早已被链接进 sbx 二进制——工具在手,只是没被用在正确的位置。
点击展开:升级与临时缓解执行命令
# 1. 升级 Docker Sandboxes 至修复版本 docker sandboxes upgrade 0.51.0 # 2. 确认当前版本 docker sandboxes version # 3. 临时缓解:复制前后手动解析真实路径 realpath -e ./workspace/target # 若解析结果不在工作区根目录之下,立即中止复制
五、Docker 沙箱逃逸事件给出的架构级行动清单
把这次 Docker 沙箱逃逸当作一次免费的架构评审,以下清单可直接纳入团队的 Agent 安全基线:
- 立即升级:Docker Sandboxes 升至 0.51.0;未升级前,自行解析路径后再执行任何跨工作区复制;
- 清点全部通道:列出智能体与真实机器之间所有搬运文件的通道,而不只是共享文件夹;
- 在写入时刻校验:每条通道都要在解析完路径的每一段之后、真正落盘的那一刻检查边界;
- 打印真实路径:每次复制成功都输出解析后的落点,让操作者有机会发现异常;
- 优先内核强制:能用
openat2/securejoin就不要停留在用户态字符串校验。
如果你的智能体从不向 VM 之外写入、且其产物从不被复制回宿主,本文所述风险不适用。但只要它的输出落到你使用的机器上,边界校验就必须存在——这不是可选项,而是准入条件。
