ARTICLE · 1035949
智谱 AI 编程工具被曝上传代码,我翻了硬盘,最重的不是代码
9 月 18 日中午,社区里传开一份对 ZCode 客户端的逆向分析:登录状态下,它会在后台静默打包整个工作区,加密后直传云端对象存储,界面上没有能关掉它的开关。当天晚上官方在用户群里发了情况说明,承认问题出在「代码库索引」的 Repo Wiki 上,致歉,并承诺近期开源代码库、请第三方审查运行情况。
我打开 ~/.zcode/v2/checkpoints/,数出 16 个目录,一个对应一个我打开过的工程。其中一个的 pending/ 里躺着一个 .tar.gz.enc,166.1 MB,生成于 9 月 18 日 09:23。
加密包解不开。但同一个目录下的清单是明文 JSON。
清单里 2,757 条记录、174.0 MB,.git/ 占 94.4%;我写的 102 个 .ts 加起来 1.8 MB。
这篇给一份六步自查。前三步只读本机留下的文件,不用装任何东西;后三步读日志和客户端本体,要动手翻代码。每一步配一条命令和一个判据,输出都能在你自己机器上复现。最后一节讲同一天晚上另一件事。
一、官方口径,和三个眼下没人能回答的问题
曝光那份分析写得很具体:登录状态下后台静默打包整个工作区,包含完整的 Git 提交历史、LFS 缓存、reflog 和部分全局配置,加密后直传云端对象存储,界面上没有开关。
官方当晚的说明把触发点定位在 Repo Wiki。它属于「代码库索引」:在本地生成仓库索引,支撑会话检查点恢复、历史版本回退和 Repo Wiki(仓库知识库)。做 Wiki 页面时可能触发仓库数据上传;页面在云端生成后数据立即销毁、不会保存;功能上线初期默认开启,部分用户受到影响,目前已修复。另外三条整改是近期开源代码库、邀请第三方评估人员审查系统运行情况、持续公开审查进展,补偿是全体用户一次周额度重置。
社区追问最多的是三件事:仓库检索为什么要连完整 Git 历史一起传;「立即销毁」怎么验证;修复具体改了哪段逻辑。
这三问我都答不了。但有一件事不用等任何人——把「它拿走了什么」从本机读出来。清单是明文的,后面几步也都能自己跑。六步的顺序和判据先摆在这儿。
▲ 前三步只读本机,后三步要动手
二、第 1~3 步:包和清单都在你本机
第 1 步,数目录。命令是 ls -d ~/.zcode/v2/checkpoints/*/ | wc -l,本机输出 16。这个数字等于你在这台机器上打开过的工程数。目录名是一串哈希,不是工程名,所以下一步得先做一次映射。
第 2 步,读 state.json 里的两组指针。每个目录下都有一份 state.json,里面有两组字段值得看:一组叫 activeUpload(老版本是 pendingUpload),指向「当前这次」还没传完的清单;另一组叫 lastAccepted,指向更早一次被接受过的清单。两组都读出来,就知道这台机器上发生过几次交接。
本机结果:rouge-cocos 那份,activeUpload 指向 9 月 18 日的清单,2,757 条;lastAccepted 指向 9 月 9 日的清单,7,204 条,其中 5,926 条在 .git/ 里。判据是两组都在——只剩一组,说明这套机制在这台机器上还没跑完过一轮。
我两组都读了,名字上得留个心:lastAccepted 里的 accepted 是服务端做的动作,本地证明不了它等于「服务端收下了」。这两个字段不是同一个意思,别当一个数用。
▲ 16 个目录,两份指向不同时间的清单
第 3 步,按顶层目录分组求和。清单里每条的字段是 path 和 sizeBytes,两条信息就够。把 path 按第一段斜杠切开分组,再求和、算占比,输出大概是这个形状:
.git/ 1,152 条、164.3 MB、94.4%;assets/ 1,032 条 6.2 MB;marketing/ 199 条 1.8 MB;references/ 230 条 0.6 MB;scripts/ 73 条 0.3 MB;design/ 31 条 0.4 MB;其余 40 条 0.4 MB。
我又往里切了一层:.git/objects 1,127 条,单个 pack 就 132.0 MB;.git/logs 两份,各 122,571 字节,对应 250 条 reflog 记录;.git/refs 一个分支指针。我写的 102 个 .ts 加起来 1.8 MB,清单里 .png 是 0 条,图片素材没走这条路。
判据:.git/ 占比接近 90%,说明这套机制搬的不是「源码索引」,是整个仓库。另外看一眼 .png 那条——如果图片也在清单里,覆盖面又比你以为的宽一圈。
▲ 一根柱子撑起整张图
顺带查出一件事:被 git 忽略,和不被打包,是两件事。我的 .gitignore 里有一节是自己加的,注释写着「AI 工具态目录(会话工作区,不入库)」,底下是 .v2c/、.zcode/、.workbuddy/、generated-images/。跑 git status --ignored 数了一下,36 条规则全部生效。
但我把同一份清单又搜了一遍。.workbuddy/ 有 6 个文件(其中一个 .workbuddy/memory/MEMORY.md,5,962 字节),references/ 有 230 条。这两个目录同样写在忽略列表里。真正被排干净的是 library/、temp/、build/、node_modules/、out/、profiles/。
我原来把 .gitignore 当成一条边界。它管的是哪些东西进版本库,管不到哪些东西离开硬盘。这两条边界由两个不同的程序执行,写在一个文件里的规则不会自动传给另一个。
▲ 左边是我写的,右边是它做的
三、第 4~6 步:日志、客户端,和你要改的三件事
第 4 步,查日志。三条命令,重点在第三条。第一条数 repoSnapshotIndexing 的出现次数,跨 15 个日志文件一共 2,216 次(按行数算只有 1,108),里面能看到 repoSnapshotIndexingEnabled 是 true。第二条数 repo-wiki,105 次,能看到这个 RPC 通道注册过、onDidChangeRepoWiki 被订阅过。这两条能证明子系统在跑。
第三条是 grep -rl 'aliyuncs\|oss-cn-\|upload-credential' ~/.zcode/v2/logs/,本机输出 0 个文件。数据到底传到哪,日志里没有。
这一条的判据就是「查不到」。它说明日志回答不了「传没传出去」,同时也把下一步指出来了:地址不在日志里,就只能去客户端本体里找。
第 5 步,扒客户端。我的是 /Applications/ZCode.app/Contents/Resources/app.asar,307.9 MB。这里有个速度上的取舍:同一件事用 grep -a 逐词扫,一次全量 3 分 23 秒;改成按 8 MB 分块读、一趟过完所有关键词,1 秒。
9 个关键词全命中了。
upload-credential 7 处、RepoSnapshotUploadClient 1 处、uploadPostObject 1 处、x-oss-signature 4 处、aes-256-ctr 3 处、rsa-oaep-sha256 2 处、ciphertext-prefix-16-byte 1 处、publicKeySpkiPem 2 处。
判据是把源码和本机产物对一遍。源码里生成信封那一段的三个值——contentAlgorithm 为 aes-256-ctr、keyWrapAlgorithm 为 rsa-oaep-sha256、nonceEncoding 为 ciphertext-prefix-16-byte——和我 pending/*.envelope.json 里那三个值一字不差。
另一条判据是「搜不到的东西」。aliyuncs.com 命中 18 处,我一条条看了,全是前端监控(RUM)、APM trace 和 DashScope 模型接口,没有一个是上传桶。
这不是漏搜。源码里表单的 key、policy、x-oss-credential 全部取自服务端返回的凭证对象,目标桶是运行时值,所以静态代码和日志里都查不到「传到哪个桶」,只能靠抓包或者服务端配合。
▲ 源码字段和本机信封字段对得上
这一步还看到客户端里另有两套上报链路,一套阿里云 RUM 前端监控,一套 OpenTelemetry 的 APM trace。它们是常规遥测,和这次的事无关。但这个发现本身有用:数据出口不止一个,被曝光的那条只是其中最重的一条。
第 6 步,改三件事。第一件是找到索引开关本身,确认它在你这个版本里可关(本机 setting.json 里能看到 repoSnapshotIndexingEnabled 和 repoSnapshotIndexingUserConfigured 两个字段)。第二件是把躺过 Git 历史里的凭据全换掉:被打包的是整个 .git,意味着这半年里任何一次误提交、任何一次粘进代码的 key 都在包里,只清理工作区没用。第三件是把敏感文件和 AI 干活的地方分开,凭据、生产配置、客户数据放到工作区之外,项目里只留指向它的环境变量。
第三条我的判断标准是一句话:这个工具的行为,我能不能自己验证一次。开源、能审计、日志能倒出来、行为能在本地复现,听着像四个形容词,实际是四个具体条件。
那个包我留着没删。删掉只是让本机目录变干净,改变不了服务端有过什么。留着是因为下次有人问「它到底拿了什么」,我可以直接把清单打开给他看。
四、同一天晚上还有另一件事:一家已经开源,一家还在承诺里
这条容易被 ZCode 的新闻盖掉。9 月 18 日晚,MiniMax 把 MiniMax Code CLI 的 v0.4.12 面向全球开放,MIT 协议,源码在 GitHub 上(MiniMax-AI/minimax-code)。它是 MiniMax Code 客户端的核心组件,官方给的数字是 FrontierHarness Eval 任务通过率 76.7%、成功任务耗时中位数 4 分 33 秒,两项都优于报告里列的公开基线。
官方给的开源理由也值得抄一遍:让开发者能够审视工具调用和权限处理,构建可靠的企业级应用,也让社区参与发现问题、贡献修复。
放一起看,这两件事刚好是一组对照。一家是在被人扒过之后承诺开源,一家是主动开源,并且把「你可以来审权限」直接写进理由。开源对安全的全部意义,说到底就是你现在能不能读到那段代码。
如果 ZCode 真开出来了,我第一时间会 grep 三样东西:上传调用点,看 snapshot/upload-credential 在什么条件下被请求;默认开关,看 repoSnapshotIndexingEnabled 的初始值走哪条分支;遥测端点,看除仓库快照外还有几条上报链路、各带什么字段。这三条和我扒 asar 用的是同一套,区别只在一点:扒 asar 是在没有源码的情况下做静态审查,能用,但比不上真源码。
日期我记下来了。承诺能不能兑现是可以被观察的。
▲ 同一天,两种开源
最后
被打包的那个项目就是《大兵防线》。它没有配远端——248 次提交、132 MB 的 pack,从 8 月 27 日第一个提交起就只在这台机器上。如果那个包真传出去了,那是它第一次离开。
▲ 那个包里装的,就是这个项目的全部来路
这六步我已经留成了脚本。路径会变,字段名会变,顺序不会:先确认本机留下了什么,再确认客户端把它送到了哪。下次再有工具被曝类似的事,照着跑一遍就行。
▲ 这些内容都跑在同一套战斗循环上
你要是也在用带「代码库索引」「仓库快照」这类功能的工具,从第 1 步开始跑。数字和我的不一样很正常,第 2 步和第 3 步的输出能对上就够了。
点不动的话,在微信「搜一搜」里搜 大兵防线 也能找到。
你机器上那个 checkpoints 目录里有多少个快照?我这边是 16 个,欢迎把你的数字贴过来对。