8月的开发者圈,供应链投毒跟连续剧一样,一部接一部。
月初,npm 蠕虫 ChainDrop 在两个小时内给435个包塞了1557个带毒版本,keyv、cacheable-request 这种每周下载量以亿计的依赖全在名单里。Semgrep8月20日,Rust 生态的 arrayref 又被端了——这个累计下载2.45亿次的小 crate,被人加了一行依赖,编译即中招。HN 上551分、500多条评论,Rust 官方罕见地当天发博客响应。Hacker NewsRust官方博客
如果你以为这只是"别人的事故",先看一个细节:ChainDrop 的持久化位置,是 .vscode/tasks.json 和 .claude/settings.json。Unit 42也就是说,你打开 VS Code、启动 Claude Code 的那一刻,病毒会再激活一次。

我把 Semgrep 安全研究团队的两份 IoC 报告、Unit 42 的逆向分析、Rust 官方博客和 HN 上的讨论全部对了一遍,这篇帮你把两件事捋清楚:攻击者到底干了什么,以及你现在该查什么。
先说 ChainDrop:它会"繁殖"
8月4日09:35(UTC),keyv@6.0.0 成为第一个带毒版本。Semgrep接下来约两小时,435个包被批量重新发布,涵盖 @cacheable/、@nebula.js/、servicetitan、qlik 等一堆正经公司的 scope。
手法不新但很顺:盗维护者账号 → 重新发布带毒版本 → 偷凭证 → 用偷来的 npm token 继续发下一个包。带毒包里多了 setup.mjs、math_init.js 两个混淆文件,挂在 preinstall 钩子上——不需要你的代码引用它,装一下、恢复一下依赖树就执行。
偷的东西清单很长:npm token、AWS 凭证(连 IMDS 和 ECS 元数据都不放过)、SSH 私钥、GitHub Actions secrets,甚至能从 CI runner 内存里抠临时凭证。SemgrepUnit 42 还发现453个公开 GitHub 仓库匹配它的回传特征——攻击者在用公开仓库当中转站。Unit 42

两个真正值得记住的点:
一是 C2 用了以太坊链上的 dead-drop,8月4日晚攻击者一笔链上交易就把整个 C2 基础设施换掉了,恶意代码一行不用改。Unit 42

二是持久化直接瞄准 AI 编程工具:往 .vscode/tasks.json 写一个"打开文件夹即运行"的任务,往 .claude/settings.json 写一个 SessionStart 钩子,两个文件互相指向对方目录里的 dropper 副本。你开一次项目、开一次 Claude Code 会话,就重新激活一次。中招项目里要清的是这5个文件:.claude/math_init.js、.claude/settings.json、.claude/setup.mjs、.vscode/setup.mjs、.vscode/tasks.json。
再说 arrayref:一次"差一个字符"的投毒
8月20日07:15(UTC),arrayref@0.3.10 发布,Cargo.toml 里多了一行依赖:proc-macro1。注意,不是那个几乎人人都在用的 proc-macro2,是差一个数字的仿冒包。仿冒包的 src 目录直接复制了正版 proc-macro2 的代码,所以你的构建一切正常,只有 build.rs 里藏的东西在编译时悄悄下载并执行了远程二进制。Semgrep

更阴的一手:攻击者把 0.3.5 到 0.3.9 的干净版本全部 yank 掉。SafeDepcargo 遇到 yanked 版本会提示"考虑升级到未被 yank 的版本"——于是正常构建的人被官方工具亲手推向带毒的 0.3.10。第一个发现者就是这么中招的。
好消息是 Rust 安全响应团队动作很快:07:15 接报,08:41 毒版本下架,在线时长86分钟;同作者的 internment@0.8.7、append-only-vec@0.1.9 也一并处理,被 yank 的干净版本已经恢复,账号已锁定。Rust官方博客坏消息是:这86分钟里跑过构建的人和 CI,以及被 yank 提示引导升级的人,就是风险敞口。
还有个背景数字:arrayref 藏在 tiny-skia、winit、sctk-adwaita 的依赖深处,egui、eframe、iced 这些 GUI 框架的项目基本都绕不开它。2.45亿次下载里,干净的 0.3.9 占约1.52亿——这个数字说明它有多普及,不等于有多少人中毒,别被标题党吓到,但该查还得查。SafeDep
这波攻击的共性,比事件本身更值得记
把两件事放一起看,供应链攻击已经完成了三次转移:
从"假冒包"到"劫持真包发新版"。以前投毒靠起个像样的假名字骗你装,现在直接偷维护者账号发新版本。这意味着锁文件只能防"旧版本被篡改",防不了"合法账号发的新毒版本"——你主动升级的那一刻,lockfile 是跟着你一起走的。
从"调用时执行"到"安装/编译时执行"。npm 的 preinstall、Cargo 的 build.rs,都不需要你的业务代码碰它一下。装依赖、编译项目本身就是触发器。
从"感染一次"到"住进你的工具链"。IDE 的 tasks、AI 编程助手的 hooks,都是打开即触发。你的开发环境越现代,给蠕虫开的门越多。
HN 上500多条评论争的最凶的也是这个:Cargo 至今没有类似 pnpm 的安装脚本 allowlist 机制(issue #13681 还开着),build.rs 沙箱化尝试过好几次都没下文。Hacker News有人开始认真讨论"新发布版本延迟几天再采用"这种最朴素的自保策略——下面自查完你会发现,这可能是性价比最高的一条。
自查清单:对号入座,十分钟搞定
Rust 用户(重点:8月20日07:15–09:25 UTC 之间编译过项目,或那前后升级过依赖的):
第一步,跑 Rust 官方给的检查命令,看本地缓存里有没有带毒 crate:
```bash
find ~/.cargo/registry/cache -type f ( -name ‘arrayref-0.3.10.crate’ -o -name ‘append-only-vec-0.1.9.crate’ -o -name ‘internment-0.8.7.crate’ -o -name ‘proc-macro1-.crate’ -o -name 'proc-macro-en-.crate’ -o -name ‘aovine-.crate’ -o -name 'arone-.crate’ -o -name ‘aronenao-.crate’ -o -name 'tinymember-.crate’ ) -print
```
第二步,有输出的话,检查机器上的痕迹:Linux/macOS 看 /tmp/rust-setup,Windows 看 %TEMP% 下的 rust-setup.ps1;网络侧关注 23.254.165.112 的 9089 和 443 端口连接记录。
第三步,命中就按失陷处理:这台机器的凭证(云凭证、SSH key、token)默认视为已泄露,轮换;它碰过的仓库和 CI 一并排查。没命中的,把 Cargo.lock 里的 arrayref 确认回 0.3.9 或之后的干净版本即可。
npm 用户(重点:8月4日之后安装/更新过依赖的):
第一步,对照名单。重灾区包括 keyv@6.0.0、@cacheable/memory@2.2.1、@cacheable/node-cache@3.1.2、cacheable-request、@nebula.js/* 全家、@onereach/* 全家等,完整名单 Semgrep 博客里列了几百条,用 lockfile 里的版本号逐一比对,只比对版本,差一个都不算。
第二步,查持久化文件。在碰过可疑依赖的项目里找这5个:.claude/math_init.js、.claude/settings.json、.claude/setup.mjs、.vscode/setup.mjs、.vscode/tasks.json。注意 tasks.json 本身是合法文件,看里面有没有名为 “Environment Setup”、命令指向 node .claude/setup.mjs 的任务。
第三步,命中即轮换。npm token、GitHub token、云凭证、SSH key 全部换掉,检查 npm 账号有没有莫名的发布记录,CI 的 secrets 也一并重置。
一个重要的豁免条款:如果你一直开着 --ignore-scripts,或者用的是7月8日发布的 npm v12,这次基本免疫。Semgrepv12 把依赖的生命周期脚本默认全拦了,要跑得显式 npm install-scripts approve 逐个批准。还在 v10/v11 的,至少给 CI 加上 ignore-scripts,需要构建脚本的白名单单独放行。
团队层面再补两条:一是查8月4日09:35 UTC 之后 CI 日志,带毒的 loader B 会让 preinstall 非零退出,"装依赖莫名失败"本身就是中过招的信号;二是如果你们用 Semgrep 的付费 Supply Chain,这两起事件官方都推了检测规则和 advisory,直接触发一次新扫描看 findings 页面。免费用户也不用慌,这两份 IoC 报告本身就是公开的,上面的自查就是照着它做的。
往后怎么防:三件现在就能做的
脚本白名单化。npm v12 的 allowScripts、pnpm 的 onlyBuiltDependencies,都是把"装依赖顺手执行任意代码"这个默认行为改成显式批准。这是目前对安装时投毒最有效的一道闸。
新版本冷却期。HN 讨论里被反复提起的土办法:非紧急依赖,新版本发布后等个三五天再升级。已有 RFC 提案把这条做成 cargo 原生能力:可配置的等待期内自动避开新发布版本。Hacker News这次 arrayref 毒版本在线86分钟、ChainDrop 爆发两小时,冷却期几乎天然免疫。代价是补丁类更新会延迟,自己权衡。
把供应链扫描当成和单测一样的基建。免费层能做的:cargo deny 盯 advisory、npm audit 跑起来、RustSec advisory 订上。要"可达性分析"(依赖有洞但你根本没调用到)这种进阶能力,目前是 Semgrep Supply Chain、Snyk 这些付费方案的领地,团队规模到了再算这笔账不迟。Semgrep

最后留三个观察信号,这个方向短期不会平静:
cargo 的安装脚本管控(rust-lang/cargo#13681)什么时候落地,这是 Rust 生态最大的缺口;
ChainDrop 那种"以太坊换 C2"的手法会不会变成标配——如果是,现有的按域名封禁的防御思路都得改。
一句话总结:这两起事件最该记住的不是恐慌,而是一个事实——现在装依赖和编译代码本身,就是攻击面。把脚本执行权收回来,比什么都重要。