令牌就在同一个堆里
Accomplish 的 Oren Yomtov 两度逃出 OpenAI 的 Codex 沙箱。其中一次是从最严格的 read-only 模式里逃出来的:不受信任的 JavaScript 调用了 v8.getHeapSnapshot(),从两者共用的那个堆里,把受信任上下文的授权令牌读了出来。另一次则让 apply_patch 自行把写入权限扩张到文件系统的根目录,接着在 .zshrc 后面加上一行。OpenAI 八天内修好了两个。但催生它们的那个性质没有补丁可打:守卫与它所守卫的代码,是同一个进程里的两个变量。Bromure Agentic Coding 让守卫跑在你的 Mac 上,让智能体跑在虚拟机里。
不受信任的那一侧从来没有攻破沙箱。它只是向 Node 要了一份堆的快照, 发现沙箱自己的授权令牌以一个再普通不过的字符串躺在那里,然后用了它。
你把 Codex 指向一个别人发给你的仓库。你很谨慎,所以用 read-only 跑它,
那是智能体能看不能碰的模式。智能体读了那些代码。其中一部分是载荷,
几秒之后,一条命令就在你的 Mac 上跑了起来,在沙箱之外,屏幕上没有任何提示。
那就是 Heapjack, Accomplish 的 Oren Yomtov 这个月公开的 两个 Codex 沙箱逃逸之一。 BleepingComputer 在 9 月 20 日做了报道。 Yomtov 在 8 月 12 日把两个都报告给 OpenAI,OpenAI 在八天内修好:Heapjack 修在 Codex Desktop build 26.818.21641,另一个修在 Codex CLI 0.149.0。OpenAI 的声明 属于简短的那一类:“我们感谢研究人员与我们联系并分享他们的发现。我们在八月处理了 这两个问题,但我们会持续不断地加固我们的沙箱。”
第一个缺陷为什么会存在,那才是比补丁活得更久的部分。
一个进程,一个堆,一个令牌
Codex Desktop 有个叫 node_repl 的工具,用来执行 JavaScript。它在里面保有两个
上下文:一个受信任,获准请求父进程去做真正的事;一个不受信任,执行智能体
当下正在求值的任何东西。这两者之间的隔阂,就是沙箱。
两个上下文活在同一个 Node 进程里。用 Yomtov 的话说:“一个 Node 进程,一个 V8 isolate,一个被两个上下文共用的堆。令牌就是那个堆里的一个字符串。”
令牌就是父进程用来分辨两者的办法。带着正确令牌从管道抵达的请求就是受信任的, 会被执行;没带的就不是。它是一个随机 UUID,只要你要瞒着的那段代码读不到你的 内存,它就是个相当不错的秘密。
它读得到。v8.getHeapSnapshot() 是 Node 的公开 API,一个剖析用的工具,它会返回
堆上每一个对象的转储,字符串也在内。不受信任的上下文调用它,滤出形状像 UUID
的东西,最后得到一小把候选。要从中挑出真的那个还得多一步:拿每一个去向父进程
试,看回来的是什么。错的令牌回你“未经授权”。对的那个,回你一句对请求其余部分
的抱怨。Yomtov 留下了产生第二种回复的那个字符串。
令牌到手之后,不受信任的上下文就把请求写进受信任那一侧在用的同一条 stdout
管道。父进程核对令牌,认定有效,然后在沙箱外执行那条命令。从那里开始:通过
open 下系统命令、Docker socket、修改配置文件。
这一切都是从 read-only 出发的。再引一次 Yomtov:“这一切都跑在 read-only
里,最严格的沙箱模式,智能体理应什么都写不了的地方。”
一个会长大的写入权限
第二个逃逸 Overpatch 就朴素多了。Codex CLI 有个编辑文件的 apply_patch 工具,
它会从交给它的补丁里所指名的路径,推算自己需要多少写入权限。那些路径来自
模型,而模型正在读那个仓库。
于是一份补丁在某个条目里指名了 /tmp,附带一个什么也不做的变更。变更失败了。
但它所隐含的权限授予并没有失败:写入权限一路扩张到文件系统的根目录。同一份
补丁里的第二个条目,接着就通过一个符号链接写进 $HOME,在 .zshrc 后面
加上一行。
到这里还没有任何东西逃出去。逃逸发生在后来,在你打开终端、你的 shell 以你的
身份执行那一行的时候,那条路径上没有任何沙箱。这一个跑的是 workspace-write,
日常的那个模式。
两个不同的缺陷,两个不同的产品,文章里有一句话同时涵盖了它们:“负责执行规范的 那个东西,就坐在被规范的那个东西里面。”
守卫与被守卫者,同处一个地址空间
一个与它所约束的代码活在同一个进程里的沙箱,是一种约定。它撑得住,直到有人 认真地盯着它看。令牌在堆里。权限的计算读的是攻击者提供的路径。管道是同一条 管道。这其中每一项都可以修,OpenAI 也都在八天内修好了。下一个这种类型的缺陷, 会是同一个堆里的另一个字符串。
Yomtov 自己的结论则是架构层面的:“这正是 Accomplish 把整个智能体跑在虚拟机 里的原因……我们把客户机里的一切都当成不受信任的,含 root 在内,因为决定什么被 允许的东西,没有一样跟着跑在里面。”
那正是 Bromure Agentic Coding 交付的设计。智能体跑在一个 Ubuntu 24.04 客户机 里,底下是 Apple 的 Virtualization.framework,macOS 自己就提供的那个 hypervisor。客户机在构造上就不受信任,含 root 在内。决定什么被允许的代码, 以主机进程的身份跑在一条 vsock 的另一侧,而客户机内部没有任何令牌能授权任何 事情,因为值得授权的一切都坐在客户机之外。
每一种逃逸换成在这里会遇到什么
Overpatch 比较具体,就从它开始。这个手法的结尾,是往 $HOME 里的 .zshrc
追加内容。在 Bromure 的工作区里,客户机的 $HOME 是 home.img,一个挂在那台
虚拟机上的 ext4 镜像。你 Mac 的家目录不在里面。客户机在你 Mac 上够得到的,是你
共享进去的那些项目文件夹:最多八个,由你挑选,列在工作区编辑器里。你打开终端时
shell 会读的那些 dotfiles 不在其中,除非你自己把它们放进去。
Heapjack 是比较难的那一个。Bromure 里没有任何东西会检测客户机内部的堆快照 手法,也不打算检测。改变的是逃逸抵达的地方。在 Codex 里,逃出来的代码落在 那个握着你 SSH 密钥与云凭据的账号里。在 Bromure 的工作区里,它落进一台这样的 Linux 虚拟机:
凭据都是诱饵
sk-ant-api03-brm-…、一个形状像 ghp_ 的字符串、一个 secret 无效的
~/.aws 配置文件。它们保留结构,所以工具会接受,但在上游一文不值。真正的
值活在主机上代理服务器的内存里,在出去的路上被换上,范围限定在它们所属的
那台主机或其子域名。
把它带出去就会触线
代理服务器会扫描每一个外发请求的头部与正文,找的就是这个工作区自己的那些 诱饵。一个诱饵若前往它不是为之铸造的地方,就会被 HTTP 451 挡下,一个字节 都不会转发。虚拟机暂停,一行红色的 凭据中介 落进安全时间线。
网络是一份你写的清单
出口是一组有顺序的规则集,结尾是 default allow 或 default deny。虚拟
交换机把虚拟机在端口 80 与 443 上的流量转进主机的代理服务器,其中没有任何
环境变量介入,也没有任何客户机关得掉的东西。编辑会立即对运行中的会话生效。
push 是一个问题,在 Mac 上问
Git 托管平台的凭据带着一份写入策略,而新工作区的默认是 写入前先询问。
git push 就是写入。对话框会指名那个确切的操作,并绘制在你的桌面上。虚拟机
里的攻击者点不到它。
真出事的时候,你靠一个菜单项就能恢复。入侵处理流程会抹掉工作区的磁盘与家 目录,并保留你的设置、令牌与 SSH 密钥。真正的凭据从来没离开过你的 Mac,所以 你不用轮换任何东西。重置磁盘 会重新克隆一份系统镜像。Bromure 的任何动作 都不会碰你共享出去的项目文件夹,提示上也是这么写的。
最严格的那个模式,才是输掉的那个
这个故事里的开发者什么都做对了。他们刻意选了那个工具所提供最严格的设置,因为
那个仓库不是他们的。read-only 是一个真实的功能,也做了真实的工作,直到
共用堆里的一个字符串,把它变成了一句建议。
模式、允许清单与批准提示都值得存在,而它们每一个都预设了:执行这些规范的代 码,坐在它所限制的代码够不到的地方。那是一个关于内存布局的主张,而你可以 去查证它。Yomtov 查了 Codex 两次,横跨两个产品,两次得到的答案都是否定的。
所以,把智能体跑在那个答案不再要紧的地方。给它 root,让它爱装什么就装什么, 让它去读那个别人发给你的仓库,然后把每一份凭据、每一个目的地的决定、每一次 push 的确认,都留在一层 hypervisor 的另一侧——下一次逃逸抵达的,会是一台你 本来就愿意丢掉的机器。