注册表在沙箱之间传纸条
Check Point 在 9 月 8 日发表的研究显示,两个 ChatGPT 的代码执行容器——分属两个不同账号、彼此之间没有网络路径——可以通过那个为它们供应包的内部 JFrog Artifactory 实例互传数据。前门什么都没坏。那些容器持有的读取凭据同时也能写入,于是存储包上的一个元数据字段就成了信箱。Bromure Agentic Coding 不给访客任何镜像上的账号,并且让你把 read-only 写进一条由线路强制执行的规则里。
隔离守住了,连接器的行为与文档相符,而中间那个服务本来就是为了安全才装上的。 受害者的邮件依然到了陌生人的账号里,走的是存储包上的一个元数据字段。
六月,Check Point 的研究人员在一个包仓库里的文件上写了一个属性。属性名叫
chatgpt_test_ts,值是一个时间戳。他们是从执行 ChatGPT 对话代码的那个沙箱内部写进去
的。接着他们用另一个账号开了一段对话,跑在它自己的容器里,把同一个属性读了回来。值完
好无损地穿过来了。
那个时间戳就是整个发现。 Check Point 在 9 月 8 日发表了它, 标题胜过任何摘要:The Shared Clipboard Inside the Sandbox。到那时 OpenAI 已经把该服 务退役。研究人员在六月下旬通报,得到的回复是内部那个 Artifactory 实例已经下线,所以 没有补丁要打,用户也没有事情要做。但它的形状留了下来。
中间那个服务
会执行代码的 ChatGPT 对话会拿到自己的容器,与其他对话的容器隔离。这部分是有效的。跑
Python 的容器也需要 pip install,而谨慎的平台团队不会让沙箱自己伸手去碰
pypi.org。于是沙箱得到一个受管控的中间人:一个内部的
JFrog Artifactory 实例,挡在公共注册表前面,把审核
过的包往内送。
这是好设计。银行会把同样的东西挡在 npm 前面,受监管的医疗机构会把它挡在 PyPI 前面。 镜像的存在是为了减少沙箱的暴露面,而它确实做到了。
它同时也给了每个容器一个镜像上的账号。
Artifactory 的 REST API 里有一组 Item Management,位于
/api/storage/{repoKey}/{itemPath},让客户端可以在存储项上附加任意的具名字符串属
性,再读回来:Set Item Properties 和 Get Storage Item Information。设置属性需
要 Annotate 权限。读取不需要。Check Point 发现,发给沙箱容器的那份凭据——本意是只读
的——两者都带着。如
Cybersecurity News 的总结
所说,“交给容器的读取凭据,其权限足以同时写入和读取这些属性”,而且“那些属性并未按账
号隔离”。
属性值可以放文本,二进制的东西则放 Base64;一份塞不进单个属性的载荷会切分到好几个属 性,在另一端重新拼回来。两个属于毫无关联客户的容器,彼此之间没有网络路径,如今握着一 条带宽上限不值一提的双向通道。
同一轮里的两段对话
攻击者的指令靠寻常途径抵达受害者:一段粘贴的提示、一个分享的对话链接,或是一个用 户看不到其构建指令的自定义 GPT。接着受害者发出一条正常消息,这就是全部的触发条件。
在 Thinking 模式下,模型在同一轮里处理了两道独立的工作流。Check Point 的概念验证让受 害者问一个做菜的问题并得到正确答案,而在那个答案旁边,模型读了受害者已连接的 Gmail 账号,把找到的东西写进一个 Artifactory 属性,等攻击者的容器来收。 领导 Check Point 恶意软件分析团队的 Pedro Drimel Neto 告诉 The Register: “一段精心构造的指令,可以让 ChatGPT 在可见的对话旁边处理第二道任务流。”Gmail 只是演 示。Google Drive、Microsoft Teams 和 GitHub 都在同一个伸手可及的范围内。
受害者看到的信号只有一个,一个小小的 “Talked to Gmail” 标签,而且是事后才出现 的。连接器默认是 Important actions 设置,会不问就批准低风险的读取;更严格的 Always ask 才会弹出询问。这个设置把一次读邮件归为低风险,却不问是谁要求的。
Check Point 对模型的描述是:它“因此可以充当一个被胁迫的内部人:并非恶意,而是被说服 去替攻击者行事”。
可以一般化的那部分
把 ChatGPT 拿掉,这个故事讲的就是一个编码代理,因为结构一模一样。
代理的沙箱必须安装包。让它毫无中介地连上公共注册表,正是你本来要避免的事,所以你在前
面摆一个镜像:Artifactory、Nexus、私有的 npm 代理,你们组织标准化成什么都行。沙箱现在
要向那个镜像验证身份,于是它持有一份凭据,而那份凭据能做什么,早在这个代理存在之前就
有人决定好了。包注册表接受上传、标签、注记和保留规则,这就让 Annotate 成了一个小小
的权限,坐在一片很宽的面上。
所以,别问沙箱有没有隔离,改问你自己的环境两件事。指出它在什么东西上持有账号。然后指 出哪些动词在那个账号上行得通。
在 Bromure 的工作区里
Bromure Agentic Coding 把每个编码代理跑在你 Mac 上一个硬件虚拟化的 Linux VM 里,并把 真正的秘密留在那条界线的主机侧。把 Check Point 的三样材料放进工作区走一遍。
一个共享的服务。 没有。每个工作区对应一个 VM,有自己的系统磁盘、自己的家目录镜 像、自己的 IP,以及主机上自己的 MITM 代理监听器。两个工作区不共享任何 VM,而手册对其 中一对的描述是它们“从不共享一个字节”。工作区底下没有一个全机队共享的镜像,带着两边都 能写入的元数据面。工作区底下坐着的,是你自己 Mac 上的一个进程,每个工作区一个。
一份镜像上的凭据。 代理一份也没有。Bromure 工作区里的包安装会经由一个 virtio
socket 出去到主机代理,由代理去跟注册表对话。Bromure 把信誉服务的 API 密钥留在主机
侧,不导出到任何 VM。Depi 这条路径是最鲜明的例子:代理在主机侧把你的注册表密钥当成
Authorization: Bearer 头附上,并在经过时剥除访客送来的任何 Authorization 头。访
客里不存在读取凭据,所以也没有哪一份能多带一个动词。
一个活得比理由还久的写入动词。 这里轮到你把约束写下来。工作区的 Guardrails 面板
带着一道有序的出向防火墙,而 web 协议的规则有一个 Methods 列。在 allow 之
下,这些方法构成一份允许清单,而 read-only 是 GET、HEAD 和 OPTIONS 的简写。手册说
明了这一列为什么存在:方法这个维度“就是让工作区能够读取一个它无权写入的 API 的东
西”,而且是在线路上,不管 VM 里是哪个工具发出请求。有两层在执行它。虚拟网络交换机以
目的 IP 和从 DNS 窥得的主机名比对每一条流,代理再以 TLS 服务器名和单个 HTTP 方法比对
一次,所以被攻陷的代理绕不过任何一层。编辑不必重启就会套到正在运行的会话上,而每一次
裁决都会落成 Security Timeline 里的一行 Firewall。
第二道流,以及回到外面的路
攻击者的第二道任务流,是以模型读到的内容抵达的。在 Bromure 工作区里,代理摄入并流式
回传给模型的一切(文件内容、网页、命令输出、任何工具返回的东西)都以 tool_result
区段的形式穿过同一个主机代理。源代码检测器会在模型依据这些区段行动之前,用本机的
PromptGuard 分类器给它们打分,然后记录命中、暂停等你决定,或是拦下请求。这个检测器出
厂是关闭的,第一次启用时会拉一个 298 MB 的模型,所以请在你需要它的那次会话之前先启
用。
未经批准的连接器读取,在这里有它的反面。Bromure 工作区的 MCP bearer 令牌在 VM 里是以
brm-mcp_ 占位符的形式出现。真正的令牌留在访客之外,而代理会在线路上,朝着那份令牌
唯一被铸造给的那台主机替换上去。为某份凭据打开 Ask before use,它在一次会话中的
第一次使用就会暂停,弹出主机侧的对话框,提供 5 分钟、1 小时或本次会话剩余时间的授
权。那就是“Important actions”拒绝显示的那个询问,逐份凭据,出现在你正坐着的那台机器
上。
万一 VM 里有什么把占位符带到了不该去的地方,代理会一个字节都不转发地拒掉请求,暂停
VM,并发出一条警报,指名令牌的预览、那份凭据、它被铸造给的主机,以及它正要前往的主
机。在那条绊线之下,设在 Activity only 的追踪会为每个请求写一行元数据:主机、方
法、路径、状态、字节数、代理替换了哪些凭据,以及任何不是 Bromure 铸造的 bearer 形式
令牌的泄漏警告。会话结束后跑一下 bromure-cli trace hostnames,代理接触过的每一台不
同主机都会连同次数打印出来。
如果你为你的代理们运营一个镜像
先查你的沙箱是用什么身份验证的,再谈它们之间的网络路径。在 Artifactory 上,去找一
找你以为是只读的令牌上有没有 Annotate,以及有没有比沙箱安装来源更宽的
permission target。属性 API 只是其中一个可写入的元数据面;保留规则、标签和构建信
息都是别的。一个能注记的读者,就能张贴。
用一行把它答完的规则
在工作区的 Guardrails 面板里,用 web 协议和 Methods 列的 read-only 为注册
表加一条出向规则,然后把 Unmatched traffic 设为 Deny,再把工作需要的东西
列出来。面板底部的 pf 格式披露会把你组出来的东西打印出来:
allow web registry.npmjs.org read-only、allow web api.github.com、
default deny。它不必重启就套用到正在运行的会话。
一个共享服务让租户变成邻居
听完这样的故事,本能是去检查墙。墙没问题。两个账号底下的两个容器彼此没有路,Check Point 也没找到。他们找到的是一个双方都有权对话的第三方,并且用了人们用共享磁盘和 DNS TXT 记录用了三十年的那种方式去用它。
Check Point 在报告里把教训写明了:“一个纯粹作为基础设施的共享内部服务,可能变成跨越 那些本该保持隔离的环境之间、非预期的通信层。”你为了安全而装的基础设施仍然是基础设 施,而它的租户们就坐在一跳之外。缩小沙箱持有账号的那组对象,并且把“这些账号能做什 么”这个决定,从两年前某人关掉的一张开通单里搬到每一个请求上。
值得拿来跑的代理沙箱,都会有一条通往它所需包的受管控路径,因为另一种做法更糟。把那条 路径当成一段附带权限的关系,并把那些权限当成你的边界。在你还看得见它们的地方做决定。 安装 Bromure Agentic Coding,别给你的代理任何可以拿来登录的东西。