ZCode 事件技术复盘:313MB 加密包里装了什么
利益声明:本文作者参与了 wescode 的开发。本文是对公开安全事件的技术分析,所有引用来源已在文末标注。
本文是「代码安全实录」系列第 2 篇。对 ZCode 2026 年 9 月事件做技术层面的拆解,所有分析基于已公开的逆向工程报告。
在系列第 1 篇中,我们按时间线梳理了 2026 年 7-9 月的五起 AI 编程工具安全事件。ZCode 事件是其中影响面最广的一起——它涉及国内开发者最常用的工作环境,引发了企业正式追责,也促使厂商做出开源承诺。
这篇文章不再重复事件经过,而是拆解三个技术问题:313MB 的加密包是怎么打出来的、里面装了什么、为什么用户无法自行验证。
一、打包机制:从工作区到 tar.gz
触发条件
根据 ferstar 的逆向分析,ZCode 的打包上传由以下条件触发:
- 用户保持登录状态
- 工作区(workspace)被打开
- 后台定时任务检测到工作区有变更(或首次打开)
关键点在于:打包逻辑不在用户主动操作的路径上。它不是"你点了某个按钮才触发",而是后台自动运行的。用户在使用编辑器的过程中,可能完全没有注意到后台正在进行文件遍历和打包。
ferstar 在分析中描述了一个场景:他打开一个项目,正常编辑了几分钟,然后通过系统监控工具发现 ~/.zcode/ 目录下出现了一个持续增长的 tar.gz 文件。他没有点击任何"备份"或"同步"按钮——打包是自动开始的。
文件遍历逻辑
ZCode 的打包采用递归目录遍历方式,从工作区根目录开始,将几乎所有文件收集到一个 tar.gz 归档中。
被包含的目录和文件:
| 包含 | 说明 |
|---|---|
| 所有源代码文件 | .ts、.py、.go、.java 等——这是预期中的 |
.git/ 目录 | 完整的 Git 对象库、引用、reflog——这是问题的核心 |
.env 文件 | 包含 API Key、数据库密码等敏感信息 |
config.yaml 等配置文件 | 包含内部服务器地址、端口等 |
node_modules/、vendor/ 等依赖目录 | 如果存在的话 |
.idea/、.vscode/ 等 IDE 配置 | 包含调试配置、运行参数 |
ferstar 报告中指出,.gitignore 规则没有被用于过滤打包范围。也就是说,即使你在 .gitignore 中排除了 .env,打包逻辑仍然会把它包含进去。
这是一个重要的细节:开发者通常依赖 .gitignore 作为"这些文件不应该被分享"的声明。但 ZCode 的打包逻辑不读 .gitignore——它做的是完整的文件系统遍历。
打包后的处理
tar.gz 归档在本地生成后,存放在 ~/.zcode/ 目录下。文件大小取决于工作区体积——ferstar 测试的项目产生了约 313MB 的归档,魔都水滴测试的更大项目产生了约 700MB 的归档。
归档的具体路径结构(根据逆向分析):
~/.zcode/
├── cache/
│ ├── workspace-{hash}/
│ │ ├── snapshot.tar.gz # 工作区完整快照
│ │ ├── snapshot.tar.gz.enc # 加密后的快照
│ │ └── metadata.json # 上传元数据
│ └── ...
└── config.json # ZCode 本地配置
随后,这个归档经过加密处理后被上传到智谱的服务器。加密机制在后文详述。
"打地鼠"现象
魔都水滴的分析中记录了一个更令人不安的现象:手动删除 ~/.zcode 目录会触发无限重传。
流程如下:
- 用户发现
~/.zcode/下有异常的大文件 - 用户删除
~/.zcode/目录 - ZCode 检测到缓存目录消失,重新创建
- 重新遍历工作区,重新打包
- 重新加密,重新上传
这意味着用户无法通过删除本地缓存来阻止上传——删除只会触发重新打包。要真正停止上传,必须退出 ZCode 进程或断开网络。
二、86.6% 的 .git 历史:这意味着什么
313MB 的归档中,真正的源代码只占约 13%。剩下的 86.6% 来自 .git 目录。很多开发者可能没有意识到,.git 目录里到底存了什么。
Git 对象库的四种对象
Git 的 .git/objects/ 目录是一个内容寻址的对象数据库。每一次 git add 和 git commit,都会往这个数据库里写入新对象。对象分四种:
| 对象类型 | 存储的内容 | 安全含义 |
|---|---|---|
| blob | 文件的完整内容(每个版本) | 🔴 一个文件改了 50 次,就有 50 份完整内容 |
| tree | 目录结构的快照 | 🟡 暴露项目结构和文件名 |
| commit | 提交元数据(作者、时间、消息、父提交) | 🟡 暴露开发者身份和工作节奏 |
| tag | 标签引用 | 🟢 一般无敏感信息 |
核心问题在 blob 对象:Git 存储的是每次变更时文件的完整内容,不是增量差异。
这意味着:
- 如果你在 v1.0 的
.env里写过DB_PASSWORD=realpassword123 - 在 v1.1 中把它改成了
DB_PASSWORD=changeme - 在 v1.2 中把
.env加入了.gitignore并从跟踪中移除
三个版本的 blob 对象全部存在于 .git/objects/ 中。 你在当前代码里看不到那个真实密码,但 Git 历史里有。执行一条 git log -p -S realpassword123 就能找回来。
一个具体的数字
假设一个项目有 3 年的开发历史、1000 次提交、50 个文件平均每个被修改过 20 次。Git 对象库中会有多少个 blob?
50 个文件 × 20 个版本 = 1000 个 blob 对象
每个 blob 是文件的完整内容(Git 在 pack 时会做 delta 压缩,但对象模型是完整内容)。如果平均每个文件 10KB,Git 对象库在打包前的原始大小约为 10MB(pack 后可能压缩到 3-5MB)。
但真正的风险不在体积,在于内容的历史穿透。你删除的密钥、你放弃的方案、你撤销的 commit——在 blob 对象里全部保留。
reflog 暴露了什么
.git/logs/ 目录下的 reflog 记录了所有引用(分支、HEAD)的变更历史,包括:
- 每次
git checkout切换分支的记录 - 每次
git commit的时间戳和作者 - 每次
git reset、git rebase等操作 - 尚未推送到远端的本地分支
- 已删除但未过期的分支的最后位置
reflog 条目的格式:
<旧SHA> <新SHA> <作者> <时间戳> <操作>: <描述>
一条典型的 reflog 记录:
a1b2c3d e4f5g6h Zhang San <zhangsan@company-internal.gitlab.com> 1719000000 +0800
checkout: moving from feature/payment-gateway-v2 to hotfix/fix-alipay-callback
这条记录同时暴露了:
- 开发者姓名和公司内部 GitLab 域名
- 时间戳(工作时间模式)
- 两个分支名(暗示了正在开发的功能和正在修复的 bug)
对于企业来说,reflog 可能暴露:
- 内部 GitLab/GitHub 服务器的域名(在 remote URL 和 author email 中)
- 开发团队的工作时间模式(时区和活跃时段)
- 尚未公开的功能分支名称(如
feature/new-payment-provider) - 内部 bug 修复的紧急程度(从
hotfix/分支命名推断)
LFS 缓存
如果项目使用了 Git LFS(Large File Storage),.git/lfs/ 目录下会缓存大文件的本地副本。这些通常是二进制资产——模型权重文件、数据集、图片、文档等。
魔都水滴的分析指出,在他测试的项目中,LFS 缓存占了 .git 目录体积的约 11.6%(约 40MB)。
LFS 对象的安全含义取决于内容类型:
| LFS 内容类型 | 风险等级 | 说明 |
|---|---|---|
| 设计稿和图片 | 🟡 | 可能包含未公开的产品设计 |
| 文档(PDF/Word) | 🟡-🔴 | 可能包含内部规范、客户资料 |
| 模型权重 | 🔴 | 可能是核心知识产权 |
| 数据集 | 🔴 | 可能包含用户数据或训练数据 |
| 构建产物 | 🟢 | 通常无敏感信息 |
一个容易被忽略的事实
很多开发者的心理模型是"我的代码就是 src/ 目录下那些文件"。但从 Git 的视角看,你的仓库 = 当前文件 + 所有历史版本 + 所有分支 + 所有操作记录。上传 .git 目录,等同于上传了项目的完整时空快照。
用一个比喻:上传当前源代码,相当于给别人看你书桌上的文件。上传 .git 目录,相当于把你的废纸篓、抽屉里的草稿、日记本上划掉的部分,一起交出去。
三、RSA 加密机制:你打不开你自己的数据
加密流程
ZCode 对 tar.gz 归档的加密采用混合加密方案:
1. 客户端向服务端请求一个 RSA 公钥
2. 客户端生成一个随机 AES-256-CTR 密钥
3. 用 AES-256-CTR 密钥加密 tar.gz 归档
4. 用 RSA 公钥加密 AES-256-CTR 密钥
5. 将加密后的归档 + 加密后的 AES 密钥一起上传
这是一个标准的**信封加密(envelope encryption)**方案。从密码学角度看,算法选择没有问题——AES-256-CTR 用于数据加密,RSA 用于密钥加密,都是成熟的方案。
问题在于密钥分配
问题不在算法,在于谁持有密钥:
| 密钥 | 位置 | 谁能使用 |
|---|---|---|
| RSA 公钥 | 服务端下发到客户端 | 客户端用它加密 AES 密钥 |
| RSA 私钥 | 仅在智谱服务端 | 仅智谱能解密 |
| AES-256-CTR 密钥 | 随机生成,用后丢弃 | 被 RSA 公钥加密后,只有持有私钥的智谱能恢复 |
这意味着一个简单的事实:
你的硬盘上那份加密文件——你打不开。
ZCode 客户端打不开。任何第三方工具打不开。全世界只有智谱服务端的 RSA 私钥能解密。
这不是加密强度的问题,是加密方向的问题。正常的端到端加密(如 Signal、iMessage)是你持有私钥,服务方无法读取。ZCode 的方案是反过来的——服务方持有私钥,你无法读取。
对比:不同加密方案的信任模型
| 加密方案 | 谁持有解密能力 | 用户能否审计内容 | 用户能否验证删除 |
|---|---|---|---|
| 端到端加密(Signal 模型) | 用户 | ✅ | ✅ 可以验证本地副本 |
| 服务端加密(ZCode 模型) | 服务方 | 不能 | 不能 |
| 客户端加密 + 用户托管密钥 | 用户 + 服务方可选 | ✅ | ✅ |
| 零知识加密(如 Tresorit) | 仅用户 | ✅ | ✅ |
为什么这个设计让用户失去控制
在 ZCode 的加密架构下:
- 你无法审计上传内容——加密是在打包之后完成的,加密前的明文 tar.gz 被临时存在内存中处理,你无法截获
- 你无法验证删除——即使智谱说"已删除",你无法通过任何技术手段验证
- 你无法解密自己的数据——即使你从网络层截获了上传的加密包,你也读不出里面的内容
- 服务端可以随时更换密钥——每次请求下发的 RSA 公钥可以不同,你无法感知
- 无法确认加密是否真的执行——在闭源客户端中,你无法确认 tar.gz 是否真的被加密后才上传,还是在某些路径上存在明文传输
对比一下另一种可能的设计:如果加密用的是用户自己的密钥(比如从用户的 SSH Key 派生,或让用户设置一个加密密码),那么至少用户可以解密自己的备份,也可以审计上传的内容。ZCode 没有选择这种方案。
四、"数据已销毁"的验证困境
智谱的承诺
2026 年 9 月 19 日,智谱发布致歉声明,表示:
- 承认"代码库索引"功能默认开启,导致未经明确同意即上传用户工作区
- 发布 3.14.0 修复版本,移除上传代码
- 承诺开源 ZCode 客户端
- 承诺引入第三方安全审查
为什么"已删除"难以验证
在云端基础设施中,"删除数据"是一个比大多数人想象的更复杂的操作:
对象存储的版本控制。主流云存储(AWS S3、阿里云 OSS、腾讯云 COS)默认或可选开启版本控制。启用后,"删除"操作只是标记一个删除标记(delete marker),原始对象仍然存在,可以被恢复。要真正删除,需要额外执行"永久删除"操作,且需要有版本控制的管理权限。
备份和快照。企业级云服务通常有自动备份策略——每日快照、跨区域复制、灾难恢复副本等。即使原始存储中的数据被真正删除,备份副本可能按照保留策略继续存在数周到数月。
CDN 和缓存。如果数据经过 CDN 或缓存层,边缘节点上可能有副本。这些副本的生命周期由 TTL 策略控制,不受原始存储删除操作的影响。
审计日志。出于合规要求,很多云平台会记录数据访问日志。这些日志本身可能包含数据摘要或元数据——文件名、大小、上传时间、关联用户 ID、访问者 IP 等。
数据库残留。即使文件被删除,元数据(文件名、大小、上传时间、关联用户、处理状态)可能仍存在于业务数据库的记录中。
日志系统。如果上传过程中有日志记录(几乎所有生产系统都有),日志中可能包含文件路径、哈希值、处理过程的中间状态。日志系统通常有独立的保留策略。
这不是说智谱一定没有删除数据。而是说:在当前的架构下,用户没有任何技术手段来独立验证数据是否被彻底删除。 你只能选择相信或不相信厂商的声明。
可以验证的方案存在吗?
存在,但需要架构层面的改变。例如:
- 客户端加密 + 用户持有密钥:如果用户持有解密密钥,至少可以审计上传了什么
- 零知识证明:服务端可以在不暴露数据内容的情况下证明数据已被删除(学术上可行,工程上复杂)
- 第三方审计 + 公开报告:由独立的安全审计机构验证数据处理流程,并发布公开报告
- 区块链存证:将数据处理的关键操作(上传时间、删除时间、操作者)写入不可篡改的账本
目前,智谱承诺的"第三方安全审查"是上述方案中最现实的一个。但截至本文发布时,尚未看到审查结果的公开发布。
五、"体验优化"和"快照索引"开关的问题
用户以为自己关掉了
ZCode 的设置面板里提供了两个看似与数据上传相关的开关:
- "体验优化"开关——描述暗示关闭后不会收集使用数据
- "快照索引"开关——描述暗示关闭后不会对代码建立云端索引
一个合理的用户预期是:关闭这两个开关后,ZCode 不会将代码数据上传到服务端。
实际发生了什么
根据逆向分析,底层的打包上传逻辑不受这两个 UI 开关控制。关闭开关只影响了部分功能的展示层行为,但后台的文件遍历、tar.gz 打包、加密和上传流程继续运行。
这与 Grok Build 的 "Improve the model" 开关失效是同一类问题,但表现形式不同:
| 工具 | 开关名称 | 用户预期 | 实际行为 |
|---|---|---|---|
| Grok Build | "Improve the model" | 关闭后数据不上传 | 关闭的是"用于训练",上传继续 |
| ZCode | "体验优化" / "快照索引" | 关闭后不打包上传 | 打包上传逻辑不受控制 |
两者的共同问题是:UI 开关的语义与底层行为之间存在断层。 用户以为自己关闭了数据上传,实际上只是关闭了某个上层功能标记。
六、企业发函追责
承明科技函件
太原承明科技有限公司在事件曝光后,向智谱发出正式函件。根据公开报道,函件提出了五项具体要求:
| # | 要求 | 意义 |
|---|---|---|
| 1 | 提供数据删除证明 | 不是口头承诺"已删除",要有可审计的技术证据 |
| 2 | 说明数据流向 | 数据在智谱内部经过了哪些系统、哪些人可以访问 |
| 3 | 提供完整操作日志 | 什么时候上传、什么时候访问、什么时候删除的时间线 |
| 4 | 说明是否涉及数据出境 | 数据是否存储在境外服务器、是否经过境外节点 |
| 5 | 承诺后续整改措施 | 防止类似事件再次发生的具体方案 |
这份函件为什么重要
它不只是一家公司的维权行动。它为其他受影响的企业提供了一个模板——当你的代码被未经授权上传到第三方服务器时,应该要求什么样的回应。
对于企业安全团队来说,这五项要求可以直接作为供应商安全评估的检查清单:你的 AI 编程工具供应商能不能回答这五个问题?如果不能——这本身就是一个信号。
法律框架
从法律角度看,未经用户明确同意上传用户数据,可能涉及以下法规:
| 法规 | 相关条款 | 适用性 |
|---|---|---|
| 《个人信息保护法》 | 第 13 条:处理个人信息应取得个人同意 | 🟡 代码本身可能不算个人信息,但 Git 历史中的 author 信息是 |
| 《数据安全法》 | 第 27 条:数据处理活动应取得数据所有者的同意 | 🔴 代码是企业数据资产 |
| 《网络安全法》 | 第 41 条:收集使用信息应明示并征得同意 | 🔴 适用 |
| 《民法典》 | 第 1032 条:隐私权保护 | 🟡 企业代码的隐私属性存在讨论空间 |
七、代码索引是否可能完全在本地完成
ZCode 上传工作区的目的是"代码库索引"——让 AI 更好地理解你的项目结构和上下文。这是一个合理的功能需求。问题在于实现方式:索引必须在云端完成吗?
答案是不必须。
本地索引的技术可行性
代码索引的核心是对源码进行解析、建立符号关系图、生成搜索索引。这些操作的计算量远低于模型推理——一个 50 万行的项目,在一台普通开发机上用 tree-sitter 解析 + SQLite FTS5 建索引,通常在几十秒到几分钟内完成。
本地索引的技术路线已经存在:
| 技术 | 用途 | 本地可行性 |
|---|---|---|
| tree-sitter | 解析数十种语言的 AST | ✅ 几十万行项目秒级完成 |
| SQLite FTS5 | 全文搜索索引 | ✅ 无需外部依赖 |
| 静态调用图分析 | 函数调用关系 | ✅ AST 级分析在本地完成 |
| ONNX Runtime + 小型 Embedding 模型 | 语义向量 | ✅ 本地 CPU 可运行 |
| SQLite HNSW | 向量近邻搜索 | ✅ 无需云端向量数据库 |
代码索引的结果(符号表、调用关系、搜索索引)占用的存储空间远小于源码本身,且不包含代码原文——可以安全地在本地保存和更新。
本地索引的一个具体例子
以一个 50 万行的 Go 项目为例,本地索引的产出物大约是:
| 索引类型 | 存储大小 | 内容 |
|---|---|---|
| 符号表 | ~5MB | 函数名、类型名、变量名、文件位置 |
| 调用图 | ~3MB | 函数 A 调用函数 B 的关系 |
| FTS5 全文索引 | ~15MB | 代码文本的倒排索引 |
| Embedding 向量 | ~30MB | 代码块的语义向量 |
| 合计 | ~53MB |
53MB 的本地索引 vs 313MB 的上传包——本地索引不仅更安全,体积还更小,因为它存的是结构化的索引数据而不是原始文件。
这意味着:代码理解能力和数据安全不是非此即彼的选择。 在本地完成解析和索引,只在模型调用时发送必要的上下文片段,是一个技术上可行的方案。
八、这个事件教了我们什么
ZCode 事件不是孤例。它暴露的是整个 AI 编程工具行业在快速发展中功能优先、安全滞后的结构性问题。
对开发者:
- 检查你正在使用的 AI 编程工具是否有后台上传行为——用系统监控工具(如 Little Snitch、Wireshark、lsof)观察网络请求
- 如果你在 2026 年 9 月之前使用过 ZCode,检查你的仓库中是否有敏感信息(API Key、密码、内部域名)。即使已从当前代码删除,Git 历史中可能仍有。具体命令:
git log -p -S "password" --all git log -p -S "AKIA" --all # AWS Key git log -p -S "sk-" --all # OpenAI Key git log --all --full-history -- "*.env" - 重要项目使用 AI 编程工具前,确认工具的数据处理方式
对企业安全团队:
- 将 AI 编程工具纳入软件资产管理——明确允许列表和禁止列表
- 对已批准使用的工具,要求供应商回答承明科技函件中的五个问题
- 考虑要求工具提供网络流量审计接口——让安全团队能看到工具在发送什么
- 制定 AI 编程工具安全评估模板,覆盖数据路径、加密方案、存储策略、删除机制
对工具开发者:
- "先做功能再补安全"在 AI 编程工具领域是高风险策略——你处理的是用户最敏感的资产
- 需要用户代码做索引?在本地做。需要调用模型?只发送必要的上下文片段
- 如果必须上传,至少做到:明确告知、用户可控、用户持有密钥、提供删除验证
- UI 开关的语义必须与底层行为一致——用户关闭"数据上传",底层就不能继续上传
信息来源汇总
| # | 来源 | 类型 | URL |
|---|---|---|---|
| 1 | ferstar ZCode 逆向分析 | 技术取证 | blog.ferstar.org |
| 2 | 魔都水滴 700MB 分析 | 技术取证 | blog.margrop.net |
| 3 | InfoQ 深度报道 | 媒体 | infoq.cn |
| 4 | 虎嗅报道 | 媒体 | huxiu.com |
| 5 | 新浪财经报道(含智谱致歉声明) | 媒体/官方 | finance.sina.com.cn |
| 6 | Git 内部原理 — Git 对象 | 技术文档 | git-scm.com |
| 7 | Git LFS 官方文档 | 技术文档 | git-lfs.com |
| 8 | 《数据安全法》全文 | 法规 | npc.gov.cn |
本文信息截至 2026 年 10 月 20 日。如各方后续发布更新、审查报告或澄清,请以最新信息为准。