技术热点落地:Rust 供应链投毒 arrayref 0.3.10——恶意 build.rs 自查与 cargo 依赖防线清单(2026-08-21)
技术热点落地:Rust 供应链投毒 arrayref 0.3.10——恶意 build.rs 自查与 cargo 依赖防线清单
热点来源:8/20 Rust 官方博客《Supply chain attack on arrayref》(内容已 curl 核对);HN 讨论帖 49374269 511 分 / 425 条评论(Algolia API 实测);官方安全公告 RUSTSEC-2026-0260 与 rustsec/advisory-db#3161(Nextron Systems 研究团队报告,博客点名致谢)。一句话剧情:arrayref 维护者 droundy 账号疑似被入侵,8/20 07:15 UTC 发布 0.3.10——这个「四个宏、零依赖」的老牌 crate 第一次引入依赖:typosquat 的
proc-macro1(冒名 dtolnay 的账号 dtolney 发布),其build.rs在编译期下载并脱离执行远程二进制。恶意版本上线 86 分钟、下载 2,285 次后被删除,同作者 crateinternment、append-only-vec及六个伞形 crate 一并清理,账号已锁定。本文目标:10 分钟完成影响自查、看懂攻击链路、给 cargo 依赖建起四层防线。
前情提要:
- 8/20:技术热点落地:Go 1.27 升级实战(2026-08-20)——同为工具链大事件:升级与验证方法论、「工具链滞后」坑位可互参
- 8/18:技术热点落地:DuckDB v2.0 预览版上手(2026-08-18)——同为「大版本变更」:升级前先读 release notes 与破坏性变更清单
- 8/13:技术热点落地:SQLite WAL-Reset 十六年潜伏 Bug 复盘(2026-08-13)——同为「静默风险」:不自查就不知道,给了自查清单方法论
适用场景与目标
它解决什么问题?Rust 的构建模型默认执行依赖的 build.rs——只要声明了依赖,无论代码是否用到,Cargo 都会拉取并构建它,构建脚本拥有完整权限(能联网、能读写文件、能访问 CI secrets)。本次攻击利用了这个模型:恶意版本上线仅 86 分钟就被清除,但任何在这 86 分钟内跑过 cargo build / cargo update / rust-analyzer 的人,构建机可能已被执行过远程二进制。本文目标是:今天完成影响自查,把「下次如何不被同一类攻击打穿」的防线立起来。
| 场景 | 收益 | 建议 |
|---|---|---|
| 任何 Rust 项目维护者 | 确认自己是否命中恶意版本 | 先跑「10 分钟自查」,再谈防线 |
| egui/eframe/iced/tiny-skia 用户 | arrayref 在传递依赖链深处,命中面最大 | 重点看坑 5(只查直接依赖不够) |
| CI / 发布管道 owner | 构建机即攻击面,防再次被投毒 | 部署 cargo-deny + 构建沙箱 |
| 安全/依赖治理团队 | 借事件建立基线制度 | 引入 vet/crev + 升级冷却期 |
不适合的场景(诚实版)
- 没有 Rust 项目、也不开 rust-analyzer 的:本事件与你无关,读参考资源里的通用供应链经验即可。
- 指望「升级到修复版就完事」的:RUSTSEC-2026-0260 的
patched = []——恶意版本是被删除而非被打补丁,正确姿势是锁回 ≤0.3.9,不是升级。 - 构建完全隔离(容器/沙箱 + 锁文件只读)的团队:影响极小,但建议把本清单的防线补到 CI 上,成本不高。
最小可行方案(MVP)步骤
先跑通(10 分钟):影响自查
- 查本地 Cargo 缓存(官方命令,命中即中招):
find ~/.cargo/registry/cache -type f \( \ -name 'append-only-vec-0.1.9.crate' -o \ -name 'arrayref-0.3.10.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 - 查 Cargo.lock(CI 与同事机器同样要查):
grep -E 'name = "(arrayref|proc-macro1|proc-macro-en|aovine|arone|aronenao|tinymember|internment|append-only-vec)"' Cargo.lock # 或看 arrayref 是否恰好锁在 0.3.10 - 跑 cargo audit(公告已入库,恶意类):
cargo install cargo-audit && cargo audit # 命中会报 RUSTSEC-2026-0260 (malicious) - 查 IOC:
ls -l /tmp/rust-setup、Windows 查%TEMP%\rust-setup.ps1与rust-setup-launch.vbs;ss -tnp | grep 23.254.165.112(C2 443 / 载荷 9089)。 - 处理:任一命中 → 断开机器、轮换该机上的 SSH 密钥与云凭据(攻击目标是构建机上的 secrets)、保留现场(缓存与
/tmp文件)供分析;未命中 → 记录基线即可。
再优化(半天):立四层防线
- cargo-deny 进 CI:bans 检查 +
build = "both"配置,任何新增 build.rs 依赖都会打断构建(见坑 3 的边界)。 - cargo vet / cargo crev:消费 Mozilla/Google 等发布的审计结论,或社区互审。
- min-publish-age(nightly 可用,稳定化 PR 已进 FCP,预计 1.100 落地):
# .cargo/config.toml [registry] global-min-publish-age = "7 days" - 构建沙箱:CI 用容器跑
cargo build;本地cargo fetch后cargo build --offline;关掉 rust-analyzer 自动执行 build 脚本(见坑 2)。
关键实现细节
攻击链路(为什么 86 分钟能造成伤害):
- 注入点只有一行。arrayref 0.3.10 的
Cargo.toml只加了一行[dependencies.proc-macro1] version = "1.0.107"。Cargo 会构建所有声明的依赖——哪怕源码里从未引用——所以src/lib.rs保持纯净的宏代码,构建照常通过。 - yank 是诱饵。攻击者用被入侵的账号把 0.3.5–0.3.9 全部 yank,于是
cargo update打印「consider updating to a version that is not yanked」,把开发者推向唯一非 yanked 的 0.3.10。报告者原话:就是这么中的。 - proc-macro1 是 proc-macro2 的机械改名复制(连 docs.rs 链接和 issue 引用都复制了),伪装成「正常构建」;但它的
build-dependencies露了馅——base64 + rustls + ureq,一个 token 解析库不该有 TLS 栈。 - payload 在 build.rs 里:base64 分片在编译期重组出
https://23.254.165.112:9089/,用接受任意证书的 rustls 客户端下载按 OS/arch 选择的二进制(linux-x86_64 →rust-crate_0.1.0,windows →_0.2.0,macos →_0.3.0/_0.4.0);Unix 落盘/tmp/rust-setup后chmod +x、spawn脱离等待、stdio 全 null;Windows 写%TEMP%\rust-setup.ps1经 VBScript 经wscript.exe启动并std::mem::forget(child)——源码注释明说这是为了逃逸 Cargo 的 job object,让 PowerShell 在构建结束后继续存活。C2 地址23.254.165.112:443作为 argv[1] 传给二阶段。
关键数字(均已核实):恶意版本在线 86 分钟(07:15:00Z 发布 / 08:41:40Z 删除);下载 2,285 次,不到 arrayref 同期流量的 10%——大多数项目锁文件里是老版本,这是本次「万幸」;arrayref 全史下载 245,777,808(crates.io API 实测,当前 max_stable 已回到 0.3.9);0.3.9 单版本约 1.52 亿下载,说明影响面之广。
IOC 速查:23.254.165.112:9089(载荷宿主)、23.254.165.112:443(C2);/tmp/rust-setup、%TEMP%\rust-setup.ps1、rust-setup-launch.vbs;SHA256:arrayref 0.3.10 = 25ad7009...9373ae、proc-macro1 1.0.107 = 61198155...b34d4。注意:proc-macro1 1.0.106 是发布前 5 小时的「试水」版本,为 proc-macro2 的干净副本,本身不恶意——别误伤。
常见坑与规避清单
| # | 坑 | 规避 |
|---|---|---|
| 1 | yank 警告被当作「升级信号」 | 升级前看 diff;对老牌 crate 的「突然 yank 一批版本」保持警惕 |
| 2 | cargo add + rust-analyzer 打开项目即触发 build.rs | 关 rust-analyzer.cargo.buildScripts.enable: false,在沙箱/容器里做依赖变更 |
| 3 | 以为 cargo-deny 能「阻止」恶意 build.rs | 它只能审计/标记,执行阻止做不到;恶意代码也能挪进 lib.rs |
| 4 | 盲目 cargo update 升级一切 | 锁文件变更进 review;等 7 天冷却期;用 cargo audit 决定何时升 |
| 5 | 只查直接依赖 | arrayref 深处传递链(tiny-skia → sctk-adwaita → winit → egui/eframe/iced),用 cargo tree -i arrayref 查 |
| 6 | 以为「版本删了 = 没事」 | 本地缓存、CI 缓存、vendor 目录里可能还躺着 .crate 文件;构建产物已可能被污染 |
| 7 | 沙箱是银弹 | 沙箱后产物仍可能被投毒(编译的是不可信代码);沙箱 + 审计 + 最小依赖一起上 |
⚠️ 坑 1:yank 本身成了武器
HN 评论区最被低估的一点:这起攻击的「传播引擎」是 yank 机制本身。攻击者不需要等开发者主动升级——把旧版本全部 yank,Cargo 的警告文案就替它完成了钓鱼。规避:把 cargo update 当作「有副作用的操作」对待:先看 cargo update --dry-run 输出、审查 lockfile diff,尤其是「某个多年未动的 crate 突然出现新版本 + 新依赖」的组合(arrayref 此前从未有过任何依赖,0.3.10 突然多了 proc-macro1——异常信号非常明显)。
⚠️ 坑 2:你还没审计,代码已经跑了
社区争论的焦点(Aeolos:cargo add + rust-analyzer 会在你审计前就执行 build.rs;kibwen 澄清 cargo add 本身只改 Cargo.toml 不构建,但 rust-analyzer 打开项目目录确实会立刻跑 build.rs)。规避:VS Code/CLI 配置 "rust-analyzer.cargo.buildScripts.enable": false(有需要再手动开);依赖变更一律在隔离环境(容器/VM)里做,本地只做代码开发。
⚠️ 坑 3:防 build.rs ≠ 防投毒
weinzierl 的清醒发言:攻击者把恶意代码从 build.rs 挪到 lib.rs(编译进二进制、跑测试时执行)只是「轻微不便」;vlovich123 反驳:build.rs 是对依赖链上所有人 100% 命中且总能拿到构建时的 secrets,运行时恶意代码还要等代码路径被调用。结论:别把宝押在「禁止 build 脚本」上(cargo-deny 的 build 检查只能标记不能阻止),正确姿势是「默认拒绝 + 显式允许」(vlovich123: disallow by default)——这正是 pnpm ignore-scripts 模式的思路,也是 cargo 社区在推动的方向。
⚠️ 坑 4:升级节奏的两种极端
「永远不升级」不是安全(beej71:版本本身可能有漏洞);「有更新就升」也不是。社区共识(pixl97 / lyu07282):新版本发布后等一周再进依赖树,让「第一批企鹅」替你踩雷。工具侧:cargo audit 告诉你该升哪个(rirze),min-publish-age 从机制上强制冷却(kibwen:稳定化 PR 17335 已进 final comment period,预计 Rust 1.100)。
⚠️ 坑 5:传递依赖才是重灾区
arrayref 的直接用户反而不多——它是 tiny-skia → sctk-adwaita → winit 链上的叶子,多数 GUI 项目(egui/eframe/iced)根本不知道它存在。规避:cargo tree -i arrayref 反向查谁依赖了它;CI 里跑 cargo audit(它遍历整个 lockfile,包括传递依赖);把 cargo deny 的 bans 检查打开,新传递依赖进树会打断构建。
⚠️ 坑 6:清理 ≠ 修复
crates.io 删掉恶意版本、unyank 老版本,但已经被下载到本地的 .crate 文件不会自己消失;CI 的缓存层、vendor 目录、Docker 镜像层里都可能躺着 0.3.10。命中自查命令后,记得 cargo clean + 清 CI 缓存 + 重新 vendor;如果构建产物已经分发(release 二进制),需要重新构建发布。
成本 / 性能 / 维护权衡
| 防线 | 成本 | 维护负担 | 覆盖 |
|---|---|---|---|
| cargo audit(CI 必装) | 免费,CI +10~30s | 低,公告库自动更新 | 已知漏洞/恶意版本,含传递依赖 |
| cargo-deny(bans + build 标记) | 免费,CI +30s | 中,白名单要维护 | 依赖数量/来源/许可/build 脚本出现 |
| cargo vet / crev | 免费,但学习曲线陡 | 高,审计责任落到团队 | 代码级审计信任链 |
| min-publish-age(7 天冷却) | 免费,nightly 可用 | 低;内部自研 crate 需加排除列表 | 挡「发布即投毒」窗口 |
| 构建沙箱(容器/bwrap/offline) | 低-中,CI 改造 | 中;跨平台沙箱是公认难点 | 限制 blast radius(防 secrets 泄露) |
权衡要点:
- 先免费后付费:audit + deny 一天内能上,覆盖 90% 的已知风险;vet/crev 是对「未知投毒」的长期投资。
- min-publish-age 的代价:新依赖发布当天不可用。团队内部刚发布的 crate 要配
exclude名单(PR 17335 支持强制放行关键更新),否则自研库互相依赖会卡住。 - 沙箱不是免费的:kibwen 直言跨平台沙箱是 Cargo 多年没啃下的硬骨头(2024H2 goal 搁置);Linux 上 bwrap/容器够用,macOS/Windows 上现实做法是「CI 容器化 + 本地关 buildScripts」。
- 生态欠账:crates.io 目前不标记「新增 build.rs / proc-macro 依赖」的版本(vlovich123 的失望),意味着在机制改善前,冷却期 + 人工 review 是唯一可靠的闸门。
一周内可执行行动清单
- Day 1:跑完「10 分钟自查」(缓存 find + lockfile grep + cargo audit + IOC 检查),记录结果
- Day 2:命中则隔离机器 + 轮换该机全部凭据(SSH/云/CI token);未命中则把基线写进团队 wiki
- Day 3:CI 接入 cargo-deny(bans 检查 +
build = "both"),历史告警清零 - Day 4:CI 接入 cargo audit(已有则确认 RUSTSEC-2026-0260 能被检出)
- Day 5:评估 min-publish-age——nightly 先行试点,或记录「等 1.100 稳定」;准备好内部 crate 排除名单
- Day 6:构建沙箱试点:CI 容器化 cargo build;本地统一关 rust-analyzer buildScripts
- Day 7:团队制度落地:升级冷却期(新依赖 ≥7 天)、lockfile diff 进 code review、
cargo update必须 dry-run
参考资源
- Rust 官方博客:Supply chain attack on arrayref(curl 200 实测)
- RUSTSEC-2026-0260 安全公告(curl 200 实测;
unaffected <= 0.3.9,patched = []) - rustsec/advisory-db 初始报告 issue #3161(GitHub API 实测:含 IOC 与 SHA256)
- SafeDep 技术分析:Malicious Rust Crate arrayref Runs a Build-Time Payload(curl 200 实测,payload 细节)
- HN 讨论帖 49374269(Algolia API 实测 511 分 / 425 评论)
- cargo min-publish-age 跟踪 issue #17009(RFC 3923)
- cargo 稳定化 PR #17335(
global-min-publish-age = "7 days",FCP 中) - cargo-deny bans 配置(build 字段)(curl 200 实测)
- crates.io arrayref(API 实测:max_stable 0.3.9,总下载 245,777,808)
写在最后:arrayref 事件最值得记住的不是「又一个供应链攻击」,而是攻击者把包管理器的正常机制(yank 警告)变成了钓鱼文案——86 分钟、2,285 次下载,靠的是一句系统提示。对个体开发者,十分钟自查 + 升级冷却期就足以自保;对团队,audit/deny 是今天的底线,vet 与沙箱是明天的标配。别等下一次投毒才建防线。