← 返回所有文章
发布于 · 作者 Renaud Deraison

预览能读到你的主目录

10 月 14 日,Noma Labs 披露了 SneakPeek,这是 Claude Code Desktop 预览窗格中的一个漏洞。一个被填充到超过 256 KB、伪装成测试覆盖率报告的 HTML 文件,会在没有脚本策略的情况下从磁盘加载,它的 JavaScript 从开发者的主目录读取 SSH 密钥、AWS 密钥、kubeconfig 和 Claude 自己的 OAuth 令牌,再通过后台信标发送出去。Anthropic 给出 CVSS 8.7 的评分,并在 8 月修复。在 Bromure Agentic Coding 中,网页在另一台用完即弃的 VM 里渲染,那里没有你的任何文件,而代理自己的主目录里放的是替身,不是密钥。

仓库看起来很正常。文件是一份覆盖率报告。开发者让代理看一下,剩下的交给了预览窗格。

一位开发者克隆了一个仓库,让 Claude Code 看看里面的测试覆盖率报告。这是再平常不过的请求。报告是一个 HTML 文件。在 Claude Code Desktop 中,当 Claude 读取或编辑 HTML 文件时,预览窗格会在对话旁边把它渲染出来,不会先询问。这份报告带着一段脚本。几秒之内,脚本就读走了开发者的 SSH 私钥、AWS 密钥、npm 和 GitHub 令牌、Kubernetes 凭据,以及 Claude 自己的 OAuth 令牌,并全部发送到攻击者控制的服务器。

这是 Noma Labs 在 10 月 14 日(周三)以 SneakPeek 为名发布的概念验证。CSO Online 当天就做了报道。Anthropic 给这个漏洞 CVSS 8.7(高)的评分,并将其影响视同在主机上执行代码。Noma 在 7 月 4 日报告了它,修复在 8 月 17 日随 Claude Desktop 1.30096.1 通过自动更新推出,在此之前第一个补丁已被绕过。GitHub 安全公告和 CVE 仍在等待发布。

漏洞已经修复了。但让它值得被找出来的那种配置,仍然存在于大多数开发者的笔记本上:一个渲染不可信内容的东西,以你的身份运行,就在你拥有的一切旁边。

三道防线和一个大小上限

预览窗格是 Electron 应用里的一个 Chromium 面板。Noma 在它周围数出三道防线,并在每一道上都找到了缺口。

针对小文件的脚本策略。 小于 256 KB 的文件会以 base64 的 data: URL 加载,并应用 Content Security Policy,也就是告诉浏览器哪些脚本可以运行的一份规则列表。这个策略会拦截脚本。大于 256 KB 的文件会被写入临时文件,再以 file:// URL 从磁盘加载,而这条路径不适用该策略。把报告填充到超过 256 KB 轻而易举,真实的覆盖率报告也常常有这么大。

可以在磁盘上畅通无阻的源。 网页的源(origin)决定它能读取什么。data: URL 会得到一个不透明的源,什么也读不到。从 file:// 加载的网页则得到了,用 Noma 的话说,“对受害者整个本地文件系统的同源访问”。脚本根据临时文件的路径推算出主目录,再用 fetch() 和 XMLHttpRequest 读取文件。

一个没有开启的过滤器。 窗格有一个请求过滤器,本来用于阻止 file:// 访问。它对经由代理服务器的开发服务器生效,对从磁盘渲染的静态 HTML 却一直处于关闭状态。

为了把数据送出去,脚本调用 navigator.sendBeacon(),这个 Web API 会在后台发出单向的 HTTP POST。应用里没有任何东西限制窗格能把请求发往哪里。Noma 的报告还提到,开发者的真实 IP 地址可以通过 WebRTC 获取。

Anthropic 在 7 月 24 日发布的第一个补丁,拦截了网页对 file:// 路径的 fetch() 和 XMLHttpRequest。同一天,Noma 把读取操作搬进一个从 blob: URL 启动的 Web Worker,这是补丁没有覆盖的上下文,三个缺陷又全部生效。8 月的最终修复一次性堵上了策略缺口、file:// 加载和 Worker 这条路径。Noma 的研究员 Eli Ainhorn 对 CSO 总结道:“防护是真的,只是没有覆盖每一条路径。”

在笔记本上:看一眼报告,信标就发出公开仓库src/ tests/coverage/index.html填充到 256 KB 以上藏着一段脚本“看一下覆盖率报告”预览窗格,在 Mac 上以你的身份运行256 KB 以下:data: URL,拦脚本超过 256 KB:file:// URL,无策略与整个磁盘同源fetch ~/.ssh/id_ed25519fetch ~/.aws/credentialsfetch ~/.kube/configfetch ~/.docker/config.jsonfetch Claude OAuth不问权限,无需点击攻击者服务器sendBeacon POST-----BEGIN OPENSSHaws_secret_access…client-key-data:…npm、GitHub 令牌加上你的真实 IP通过 WebRTC代理没有运行任何命令。读文件的是渲染器,用的是你账户的文件访问权限。
笔记本上的 SneakPeek。一份填充到超过 256 KB 的覆盖率报告绕过脚本策略,从磁盘加载并获得整个主目录的访问权限,读走开发者的密钥,再通过信标发出。开发者做的,只是让代理看一个文件。

不是代理干的

SneakPeek 没有用到提示注入。代理读了一个你让它读的文件,而它所在的应用把那个文件渲染了出来。脚本在预览的渲染进程里运行,用的是应用对你磁盘的访问权限。

Sysdig 的 Crystal Morin 告诉 CSO,代理层面的日志可能不会记录这次执行,因此防守方必须在运行时层监控文件读取和外连。你的会话记录只会显示一个无害的请求和一个无害的回答。

再谨慎的模型、再严格的权限规则、再加上一个逐条批准命令的人,都改变不了什么。渲染器就在 Mac 上,以你的身份运行,紧挨着你的主目录和你在那里积累的每一份凭据。

Ainhorn 给 CSO 的建议针对的正是后半部分:不要把长期有效的凭据放在开发者的机器上,改用范围受限、有效期短的令牌。

同一份报告,放进 Bromure 机器

在 Bromure Agentic Coding 中,每个代理都在你 Mac 上自己的 Linux VM 里运行,这个 VM 叫做机器;而负责渲染网页的内置浏览器,则在旁边第二台用完即弃的 VM 里运行。把 Noma 的仓库克隆进一台机器,让 Claude Code 看看覆盖率报告。

读报告就是读文本

代理读取 coverage/index.html 的方式和读取任何文件一样:作为它上下文里的文本。聊天中没有任何东西会渲染它。如果你在 Files 窗格点击这个文件,详情视图会把 HTML 显示为高亮的源代码,就和显示 .py 或 .json 一样。页面上的脚本一个也不会运行。超过 512 KB 的文件根本不会预览。一份为了躲过脚本策略而填充到超过 256 KB 的报告,看起来就是 300 KB 的标记。

渲染发生在另一台机器上

当你确实想看渲染后的报告时,代理会从它的机器提供那个文件夹,并在浏览器窗格中通过机器的 IP 地址打开。浏览器是完整的 Chromium,运行在它自己的附属 VM 里。手册中写道:“代理写入主目录的文件,不在浏览器的磁盘上。”

脚本会运行,但找不到任何东西可读。网页的源是 http://192.168.64.x:8000,一个普通的 Web 源。Chromium 不允许它读取 file:// 路径。即便有脚本通过 Worker 或浏览器漏洞突破了这一点,它落脚的也只是浏览器镜像的一个临时写时复制(copy-on-write)克隆:没有 ~/.ssh,没有 ~/.aws,没有 kubeconfig,没有 OAuth 令牌,也没有你的任何项目。它从临时路径推算出的主目录,属于一台空空如也的 Chromium VM。信标发出去时是空的。

浏览器默认不保留任何配置文件。Stay signed in to websites 处于关闭状态,所以浏览器一关,Cookie 和登录状态就消失;隐藏的浏览器在五分钟后会被销毁。除非你在机器的 Browser 设置中允许摄像头或麦克风,否则 WebRTC 一直是关闭的,那个获取 IP 地址的手法也就无从调用。

代理的主目录里放的是替身

现在把同一段脚本对准代理自己的 VM。假设仓库的测试命令会自己生成并打开报告,让代码在代理工作的地方运行。在那个主目录里逐项检查 Noma 的清单:

  • SSH 私钥。 根本没有。SSH_AUTH_SOCK 背后是一座通往 Mac 的代理桥接。VM 可以列出身份、请求签名,但私钥的字节从不离开主机。ls ~/.ssh/id_* 什么也找不到。
  • AWS。 ~/.aws/config 指向一个辅助程序,它给出真实的访问密钥 ID,搭配一个 40 个字符的假密钥。Mac 会在请求发往 AWS 的途中,用真实密钥重新签名每一个请求。
  • Kubernetes。 ~/.kube/config 是合成的。kubectl 与 Mac 上的代理服务器通信,真实的集群凭据由代理服务器保管。
  • Docker、git 和 npm 令牌。 ~/.docker/config.json 和 ~/.git-credentials 里放的是外形和真品一样的替身。你导入的令牌,无论来自 .env 还是手动输入,都会变成仅限于各自主机的替身。
  • Claude 的 OAuth 令牌。 它们留在 Mac 上。代理使用一个占位密钥运行,发往 Anthropic 的请求,由代理服务器把真实的访问令牌放到线路上。

替身同时也是绊线。Mac 上的代理服务器会扫描每一个离开机器的请求,请求头和请求体都看,找出被发往非签发主机的替身。一个带着 brm-docker-… 密码或假 GitHub 令牌前往攻击者服务器的信标,会收到 HTTP 451。什么都不会被转发,VM 暂停,Mac 上弹出标题为 This environment may have been compromised 的警报,列出令牌预览、凭据名称、它所属的主机,以及它被发往的主机。这次检测会在 Security Timeline 中留下一行红色记录。在机器的 Outbound connections 防火墙里把 Unmatched traffic 设为 Deny,连往项目没用到的主机的连接,在这一切发生之前就会被拒绝。

这回应了 Morin 关于运行时层的观点。检查就在 Mac 上、在线路上,在代理之外,也在 VM 里任何渲染的东西之外。

在 Bromure 中:渲染器没有文件,代理没有密钥机器 VM(代理)index.html 作为文本Files 窗格:源代码主目录~/.ssh/id_* (无)~/.aws 假密钥~/.kube/config 合成docker, git 替身Claude 密钥 占位在 :8000 提供 coverage/没有真秘密可读浏览器 VM(一次性)http://192.168.64.x:8000脚本运行file:// 被拒全新克隆,无用户文件无配置文件,WebRTC 关sendBeacon(…) → 空的隐藏 5 分钟后即销毁Mac代理服务器换上真密钥只给对的主机替身发错主机:451、VM 暂停、警报防火墙:拒绝陌生主机Security Timeline:红行,列出两主机保管在 Mac,两个 VM 之外SSH 私钥 · AWS 密钥 · 集群凭据 · 镜像仓库与 git 令牌 · Claude OAuth 令牌 · Security Timeline
同一份报告在 Bromure 中。代理把它当作文本读取,Files 窗格把它显示为源代码。真要渲染时,是在一台没有你任何文件、用完即弃的浏览器 VM 里进行。代理的主目录里放的是替身,替身一旦被发往错误的主机,机器就会停下。

修复,以及下一个预览

Anthropic 在 8 月修复了 SneakPeek,Noma 也重新测试过。从报告到修复用了六周,第一个补丁在发布当天就被一个 Web Worker 攻破。渲染器的修复往往如此。浏览器引擎有很多方式启动脚本,也有很多方式发出请求,而每一道防线都必须全部覆盖。

编程工具不断加入会渲染东西的窗格:HTML 预览、带嵌入内容的 Markdown、笔记本输出、开发服务器、代理刚做好的应用截图。每一个都是一个渲染器,对准开发者没写过、来自一分钟前才克隆的仓库的内容。在笔记本上,每一个都在主目录旁边运行。

在 Bromure 中,渲染网页的窗格在一台没有你任何文件的 VM 里运行,而代理工作的机器里只有替身。在那里,渲染器的漏洞暴露出来的,只是一个用完即弃的 Chromium,和一串没有任何服务器会接受的字符串。

是源代码,不是网页

代理把 HTML 当作文本读取,Files 窗格把它显示为高亮的源代码。打开报告来检查,不会运行它的任何脚本。

磁盘上什么都没有的浏览器

网页在另一台用完即弃的 Chromium VM 里渲染。代理的文件和你 Mac 上的文件都不在它的磁盘上,关闭后它什么都不记得。

满是替身的主目录

没有 SSH 私钥,AWS 密钥是假的,kubeconfig 是合成的,镜像仓库和 git 令牌都是替身。真实的凭据加密保存在 Mac 上。

线路上的警报

替身被发往非签发主机,就会收到 451。VM 暂停,警报列出两个主机,Security Timeline 留下记录。

看看那份报告

“看一下覆盖率报告”大概是对编程代理最平常的请求了。开发者会继续问起他们没写过的仓库里的文件,工具也会继续为了帮上忙而渲染这些文件。

在 Bromure 机器里问吧。在那里,网页在一台没东西可读的 VM 里渲染,代理的密钥都是替身。想进一步了解替身和它们触发的警报,请阅读那个泄露了自己密钥的代理和钥匙就是你的 .env 文件。