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

登录就已经够了

一个在清理磁盘空间的开发者,在一个隐藏目录里发现了 700 MB,然后顺着线头一路拉下去:Z.ai 的 ZCode 一直在把他整个工作树打成 tar,42,411 个文件里有 86.6% 是 .git,用一把只有 Z.ai 掌握的密钥加密,然后直接投进一个阿里云存储桶。没有攻击者,没有 CVE,没有恶意软件。触发条件就是处于登录状态。客户端把归档加密了,所以没有扫描器读得懂,剩下的事实只有目的地和大小。那正是 Bromure Agentic Coding 为工作区发出的每一个请求所记下的字段中的两个,而且记在一条 VM 里的工具无从选择的线路上。

你的编码工具不需要被入侵,就能把你的源代码送到别处去。它只需要一个默认设置。信不 信任这家厂商不是问题。要问的是:你会发现吗?

你正在一块满了的磁盘上清空间。主目录里有个隐藏目录占了七百多 MB,而你不记得装过任 何那种大小的东西。在它里面,一个叫 checkpoints 的文件夹底下,只有一个 313 MB 的 文件,扩展名是 .enc,旁边一条元数据记录写着 failureCount: 564。

五百六十四次尝试,做的是你从来没要求过的事。

七百 MB,还有一根可以拉的线

那就是代号 ferstar 的中国开发者在 9 月 18 日开始的地方,而 他对自己所发现之物做的逆向工程记录 值得整篇读完。那个目录属于 ZCode,也就是 GLM 模型背后那家公司 Z.ai 的桌面 AI 编码客户端。CSO Online 和 InfoWorld 两边都一字不动地引了他对这个行为的总结:

“只要你处于登录状态,ZCode 就会悄悄把你整个工作区打包 — 完整的 .git 历史、LFS 资产缓存、reflog,以及全局应用配置 — 加密,然后直接上传到阿里云 OSS。”

触发条件就在第一个分句里。不是你问的某个问题,不是你打开的那个索引功能:一个有效的 会话令牌,那是捕获进程唯一在等的东西。他的日志里有一个会话记下了 62 次捕获事件。当 他把归档删掉,客户端在半小时内又造了一个新的,并把失败计数加一。

盒子里装了什么

他拆开的那份快照里有 42,411 个文件,加密前 345 MB,加密后 313 MB。这 345 MB 是由什么组成的:

  • .git/lfs/:196.1 MB,56.8%
  • .git/objects/:102.2 MB,29.6%
  • .git/logs/:0.6 MB,0.2%
  • 其余全部,也就是真正的源代码与文档:46.2 MB,13.4%

这次上传有 86.6% 是 .git。 那份归档带走了仓库历史里的每一个对象、LFS 缓存拉 下来的每一个大型二进制文件,还有 reflog——它列出你待过的每一个分支,以及你做了又撤 掉的每一个 commit。读你工作树的人知道你正在交付什么;读你 .git 的人知道你试过什 么、你回退了什么、以及是谁提交的。

一个登录令牌、四个步骤、没有攻击者你的机器,已登录工作目录.git/lfs, .git/objects, .git/logs代码、docs、全局应用配置42,411 个文件 · 345 MB设置里的两个开关体验优化 → 训练用途快照索引 → 服务器端索引两个都不管打包这件事1 · zcode.z.ai返回一份 OSS 表单签名以及一把 RSA 公钥私钥那一半留在服务器2 · 本机打包并封装tar.gz → AES-256-CTR密钥 ← RSA-OAEP-SHA256313 MB,你读不了3 · POST 到阿里云 OSSzcode-prod 存储桶直接 — 不经过 Z.ai 的服务器每次捕获事件一个对象4 · OSS 回调告诉后端对象已经落地快照在他们那边被登记并且可以建索引是什么把它启动的一个有效的登录令牌。不是提示词,不是问题,也不是那个索引功能。同一个会话里 62 次捕获事件。把归档删掉,三十分钟内就被重建。failureCount: 564
归档是怎么离开这台机器的。ZCode 客户端向 zcode.z.ai 索要上传凭证,收到一份 OSS 表单签名以及一把 RSA 公钥;它把工作目录打成 tar,用那把公钥包起来的密钥在本机以 AES-256-CTR 加密归档,然后把结果直接投进阿里云的对象存储,绕开 Z.ai 自家的应用服务器,再由一个 OSS 回调告诉后端对象已经落地。对应的私钥从来不会到客户端手上,所以用户无法解开自己的归档。触发条件是一个有效的登录令牌,而不是你对助手做了什么。有一个会话记下 62 次捕获事件,而把文件删掉会在大约三十分钟内引发重新打包。

两个开关管的都是他们那一边

ZCode 发布时带着两个设置,一个仔细的开发者会找到并关掉它们:一个是体验优化,一个是 仓库快照索引。两个都真的在做某件事,而两个都没在做名字暗示的那件事。

第一个管的是 Z.ai 是否被授权使用那些数据。第二个管的是快照送到之后服务器要不要建 索引。打包和上传照跑,因为那个捕获边车只要令牌提供方递给它一个有效的 JWT 就会启动, 整条路径上任何地方都没有用户偏好。把两个开关都关掉,客户端还是把你的仓库打成 tar、 还是加密、还是投进存储桶,并且承诺对之后发生的事保持低调。

这是一家厂商的缺陷,但这个形状到处都在重演。住在它所限制的那个程序内部的控制项,只 是一个请求。它跑在同一份代码上——而那份代码若没出错,你根本不需要它。

一份快照:42,411 个文件,加密前 345 MB.git/lfs — 196.1 MB.git/objects — 102.2 MB代码 46.2 MB56.8%29.6%13.4%离开这台机器的东西有 86.6% 是仓库的历史 — 对象、LFS 缓存、reflog光是 reflog 就列出你待过的每一个分支,以及你做了又撤掉的每一个 commit。全局应用配置文件也一起被带走了。
345 MB,其中只有 46 MB 是代码。ferstar 恢复出来的那份快照里有 42,411 个文件:196.1 MB 的 Git LFS 缓存、102.2 MB 的 Git 对象、0.6 MB 的 reflog,占了整份载荷的 86.6%,对上真正的源代码与文档 46.2 MB。一个会上传你工作目录的工具,等于上传你在那个仓库里写过的一切,连你放弃的分支都算进去。

线路上没有任何东西读得懂它

客户端在每一份归档离开机器之前就用 AES-256-CTR 把它加密,并用 zcode.z.ai 交给它 的一把 RSA-OAEP-SHA256 公钥把对称密钥包起来。对应的私钥从来不会下来。ferstar 打不 开自己的归档;唯一打得开的那一方,是握着存储桶的那一方。

这排除掉你手上所有靠读字节运作的防御。密钥扫描器在那个 tarball 里找不到任何 AWS 密钥,而深度包检测看到的,是一条通往存储端点、载着被密文包住的密文的 TLS 会话。

活下来的事实有两个:它去了哪里,以及它有多大。一个 313 MB 的 POST 发往你的 团队根本不部署的区域里的对象存储主机,是会显眼的——前提是有人把你的工具连去哪里、 发了多少写下来。没有人写,所以找出这件事靠的是磁盘空间考古学和一个星期的逆向工程, 而不是瞄一眼日志。

出口在哪里

Bromure Agentic Coding 把代理和它的工具链放进你 Mac 上的一个 Ubuntu VM 里,并且只 给那个 VM 一条通往互联网的路:一个跑在主机上、在客户机之外的 man-in-the-middle 代理,位置在 VM 里任何进程都无法重新配置的地方。

这对软件没有任何要求。虚拟网络交换机会把 VM 的 80 端口与 443 端口流量透明地导进 主机的代理,不需要任何环境变量,也没有任何客户机能取消的东西,而且在每个工作区都 默认开启。一个忽略 HTTPS_PROXY、自己开 socket、直接跟存储端点讲话的工具,还是会 经过它,因为拦截住在客户机插进去的那台交换机里,而不是住在客户机里。

接着,一个没人要求的 313 MB POST 会遇到四件事。

每个请求都留下记录

每个经过代理的请求都留下一条追踪记录:时间戳、主机、端口、方法、路径、状态、 延迟,以及在任何凭证替换之前量到的请求字节数。你不需要解开一份归档,就能 注意到有个仓库大小的上传发往一台你从来没部署过的主机:大小和目的地都是列。用 一个排序,用另一个筛选。

它根本没离开 Mac

每个工作区各自的出向防火墙是一张有序规则表(动作、协议、主机或 CIDR、端口、 HTTP 方法),结尾是 default allow 或 default deny。用白名单的方式跑一个 工作区,那份上传就无处可去。有两个地方在执行规则:交换机比对目的地 IP 和从 DNS 嗅到的主机名,代理再比对一次 TLS 服务器名称,而你的修改会立刻送到正在跑 的会话。

每个判决都是一行

每一个防火墙决定都落进 Security Timeline,所以一个一直重试的工具——比方说 564 次——会以对某个具名主机的重复拒绝出现,而不是以一块慢慢被填满的磁盘出现。 那个花了一星期才重建出来的模式,正是那个窗口生来要显示的东西。

它打包的那些配置都是诱饵

ferstar 那句话里最该让你担心的词是“全局应用配置”。在一个 Bromure 工作区里, ~/.git-credentials、~/.docker/config.json、~/.kube/config 和 ~/.aws/config 都存在也都能用,而它们里面的每一个值都是保留结构的假货。VM 里没有任何文件、环境变量或进程,会握着真的 API 密钥、OAuth 令牌、AWS 机密或 SSH 私钥。

剩下的由范围处理。一个工作区就是一个项目,或者一道凭证边界;两个工作区绝不共用一个 VM,而客户机的主目录是它自己的镜像文件。一个工具在工作区里打进 tar 的东西就是那个 工作区,这就把你 Mac 上另外四十个仓库、你主机的 ~/.aws/config、还有你的 SSH 密钥 留在外面。那些密钥根本不会进到客户机:签名是在主机上通过一道 vsock 桥完成的,私密 的字节留在那里。

Mac 上的那个工具主目录~/work/* : 每个仓库、每个 .git~/.aws/config, ~/.docker, ~/.ssh : 真的工具自己选择读什么、发到哪里。工具自己执行自己的设置。313 MB,已加密,发往一个存储桶没有目的地的记录。没有大小的记录。你怎么发现的磁盘满了,于是你开始做逆向工程。工作区里的那个工具工作区 VM — Ubuntu 客户机,一个项目/home/ubuntu — 这个仓库、这份检出~/.aws/config, ~/.docker — 占位值工具还是自己选择读什么、发什么。路线不是它选的。:80 与 :443 被导向主机端代理主机 · 方法 · 路径 · 请求字节 → tracedeny web *.aliyuncs.com · default deny在线路上就被拒绝,并写进 Security Timeline。
同一个工具,两种摆法。直接摆在 Mac 上跑,它就坐在你拥有的每一个仓库、主目录里每一个凭证文件,以及一条没有中介的互联网路线旁边;上传就是一条和其他一样的 TLS 会话,而归档已经加密,所以任何读内容的东西都帮不上忙。摆进一个 Bromure 工作区里跑,它的 80 端口与 443 端口流量会被导进一个它没办法重新配置的主机端代理:代理记下目的地和字节数,出向规则决定这个请求到底走不走得出去,而它打包的那些配置文件里装的是占位值,真正的值从来没进过那个 VM。

Z.ai 接下来做了什么

Z.ai 在 ferstar 发表的当天就道歉了。公司在 ZCode v3.14.0 里关掉了那条上传流程; InfoWorld 报道这个版本在 9 月 19 日发布,更新项里列了一条修正仓库上传异常的问题。 公司并请来安全公司 NSFOCUS 审计结果。NSFOCUS 确认“zcode-prod 这个阿里云 OSS 存储桶 里的所有数据对象,以及存储桶本身,都已删除”,而 Z.ai 说审计之后“没有识别出任何能够 触发生成本机仓库快照,或把本机文件往外传送的功能路径”,并且那份代码“从未被用于模型 训练”。接着他们把整套开源了:客户端、后端、共享 UI、Agent CLI 与运行时,采 Apache-2.0,放在 GitHub 上。这高于业界常态,也 意味着下一个想查的人不需要反汇编器。

Semgrep 的 Cris Thomas 在 CSO 的报道里被引用时,给这个类别起了名字:“这其实不是 AI 模型的问题,而是一个老派的安全架构问题。”他希望厂商公开什么东西离开了机器、去了 哪里、保留多久,而且“默认打开的是最小权限,不是最大权限”。这件事没有客户能从外面 强制。

默认设置就是威胁模型

这里没有人攻击任何人。没有人偷凭证、没有人污染包、没有人把模型说服成什么。一个产品 做了它的作者把它造出来要做的事,而他们把它造出来要做的,是把你的仓库送到一个你读不 到的地方。这起事件是一个默认设置,而挑选默认设置的,是利益跟你的只是相邻的人。

这就是把边界放到工具之外的理由。你不必相信你的编码助手是敌人;你只需要注意到,看不 到的东西你没办法审计,而厂商自己的设置面板是个放那个开关的怪地方。让代理和它的工具 链跑在这样的地方:它们开的每一条连接都要穿过一条你拥有的线路,那次穿越的记录写在你 的 Mac 上,而那条挡下 313 MB 上传的规则,是你自己敲进去的一行,不是别人替你尊重的 一个勾选框。

那么“我的编码工具在发什么?”就变成星期二下午打开的一个窗口,而不是一个研究项目。