post cover

技术热点落地: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-0260rustsec/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 次后被删除,同作者 crate internmentappend-only-vec 及六个伞形 crate 一并清理,账号已锁定。本文目标:10 分钟完成影响自查、看懂攻击链路、给 cargo 依赖建起四层防线

前情提要


适用场景与目标

它解决什么问题?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 分钟):影响自查

  1. 查本地 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
  2. 查 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
  3. 跑 cargo audit(公告已入库,恶意类):
    cargo install cargo-audit && cargo audit
    # 命中会报 RUSTSEC-2026-0260 (malicious)
  4. 查 IOCls -l /tmp/rust-setup、Windows 查 %TEMP%\rust-setup.ps1rust-setup-launch.vbsss -tnp | grep 23.254.165.112(C2 443 / 载荷 9089)。
  5. 处理:任一命中 → 断开机器、轮换该机上的 SSH 密钥与云凭据(攻击目标是构建机上的 secrets)、保留现场(缓存与 /tmp 文件)供分析;未命中 → 记录基线即可。

再优化(半天):立四层防线

  1. cargo-deny 进 CI:bans 检查 + build = "both" 配置,任何新增 build.rs 依赖都会打断构建(见坑 3 的边界)。
  2. cargo vet / cargo crev:消费 Mozilla/Google 等发布的审计结论,或社区互审。
  3. min-publish-age(nightly 可用,稳定化 PR 已进 FCP,预计 1.100 落地):
    # .cargo/config.toml
    [registry]
    global-min-publish-age = "7 days"
  4. 构建沙箱:CI 用容器跑 cargo build;本地 cargo fetchcargo 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-setupchmod +xspawn 脱离等待、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.ps1rust-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 的干净副本,本身不恶意——别误伤。

常见坑与规避清单

#规避
1yank 警告被当作「升级信号」升级前看 diff;对老牌 crate 的「突然 yank 一批版本」保持警惕
2cargo add + rust-analyzer 打开项目即触发 build.rsrust-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

参考资源

写在最后:arrayref 事件最值得记住的不是「又一个供应链攻击」,而是攻击者把包管理器的正常机制(yank 警告)变成了钓鱼文案——86 分钟、2,285 次下载,靠的是一句系统提示。对个体开发者,十分钟自查 + 升级冷却期就足以自保;对团队,audit/deny 是今天的底线,vet 与沙箱是明天的标配。别等下一次投毒才建防线。