cargo build しただけで感染する — arrayref 乗っ取りに学ぶビルド時サプライチェーン攻撃
はじめに
2026年8月20日 07:15 UTC、Rust の arrayref クレートに バージョン 0.3.10 が公開された。累計ダウンロード数はおよそ2億4,500万回。配列のスライスを固定長参照に変える、それだけの小さなユーティリティだ。
この 0.3.10 には、悪意あるコードは一行も含まれていなかった。追加されたのは、Cargo.toml の依存行がたった1つだけである。にもかかわらず、この版を引いたプロジェクトは cargo build を走らせた瞬間に、リモートから落としてきたバイナリを実行させられる状態にあった。
crates.io のセキュリティチームがこの版を削除したのは 08:41:40 UTC。公開から86分後だ。被害の実測報告はほぼ出ていない。それでもこの事件が読む価値を持つのは、攻撃者が突いたのが実装のバグではなく、Rust のビルドモデルそのものが持つ設計上の信頼境界だったからだ。そして同じ穴は、npm にも、Dart の build_runner にも、Python の setup.py にも空いている。
本記事では、86分間に何が起きたのかを時系列で押さえたうえで、build.rs がなぜ「実行時サンドボックスが一切効かない」攻撃面になるのか、攻撃者が registry の安全機能である yank をどう配送路に転用したのか、そして自分のプロジェクトとCIで今日から打てる手を整理する。
86分間に何が起きたのか
事実関係を、Rust Security Response WG の公式ポストに沿って並べる。
- 07:15:00 UTC —
arrayref@0.3.10が公開される。公開者は正規メンテナのアカウントdroundy - 07:15 UTC — ほぼ同時刻、crates.io に通報が入る
- 07:34:07 UTC — 同じアカウントから
internment@0.8.7が公開 - 07:37:49 UTC — 同じく
append-only-vec@0.1.9が公開 - 08:41:40 UTC —
arrayref@0.3.10削除(公開から86分) - 09:04:11 UTC —
internment@0.8.7削除(90分) - 09:25:24 UTC —
append-only-vec@0.1.9削除(107分)
Rust 側の見立ては明快だ。
We do not believe the author of arrayref to be acting maliciously, but their computer or credentials are likely compromised (arrayref の作者が悪意を持って行動しているとは考えていないが、その計算機か認証情報が侵害された可能性が高い)
crates.io は該当バージョンの削除に加え、攻撃者が悪意を持って yank した過去バージョンを unyank し、アカウントを予防的にロックした。この「悪意ある yank」が何を意味するかは後述する。
重要なのは、arrayref が基盤クレートだという点だ。報告者は tiny-skia → sctk-adwaita → winit という依存の連鎖を辿っており、この経路は egui/eframe や iced といった Rust GUI アプリのほぼ全体の下敷きになる。他にも blake3、blake2b_simd、Ethereum 系の revm-precompile、Solana 系の solana-runtime / spl-token が依存に名を連ねる。暗号資産のランタイムが射程に入っていた、という事実は攻撃者の狙いを考えるうえで示唆的だ。
悪意あるコードが一行もない、という設計
では 0.3.10 は何をしたのか。追加されたのは、proc-macro1 バージョン 1.0.107 への依存だけである。
proc-macro1 は、Rust エコシステムで最も広く使われるクレートの1つ proc-macro2 のタイポスクワットだ。公開したのは dtolney というアカウントで、これは proc-macro2 の実際の作者である David Tolnay 氏のアカウント dtolnay と一文字違いに作られている。中身は proc-macro2 を機械的に find-and-replace でリネームしただけのコピーで、ドロップイン置換として正常に動作する。ビルドは通り、警告も出ない。
悪意は、proc-macro1 の build.rs にだけ存在した。
ここが本題だ。Rust の build.rs はビルドを実行しているマシン上で、そのユーザーの権限のまま、コンパイル時に実行される。crate の関数を一度も呼ばなくてよい。use すらしなくてよい。依存グラフに入っていて cargo build が走れば、それだけで実行される。SafeDep の分析はこう表現している。
The code runs at build time, so simply compiling a project that pulled the bad versions is enough to trigger it. (コードはビルド時に実行されるため、問題のバージョンを引いたプロジェクトをコンパイルするだけでトリガーされる)
つまり、実行時にどれだけ厳格なサンドボックスを敷いていても意味がない。攻撃はサンドボックスの手前、コンパイラが動く場所で完了している。そして arrayref 本体のコードをいくら精読しても、悪意ある行は見つからない。監査対象が本体ではなく、依存の依存の build.rs だったからだ。
ペイロードの実装
build.rs の中身も、教科書的なまでに丁寧に作られている。
C2 アドレスの秘匿: サーバーアドレスを base64 の断片として保持し、ビルド時に再構成する。文字列 grep での検出を避けるためだ。復元されるのは、ペイロード配信元 23.254.165.112:9089(HTTPS)と、C2 23.254.165.112:443。
証明書検証の無効化: 生 IP に自己署名証明書を載せているため、通常の TLS 検証は通らない。そこで rustls の ServerCertVerifier トレイトを実装した AcceptAll という独自検証器を用意し、すべての証明書・署名チェックで無条件に成功を返す。
プラットフォーム別の実行: Unix 系では取得したバイトを /tmp/rust-setup に書き出し、実行権限を付与し、C2 アドレスを第1引数に渡して待たずに起動する。Windows では PowerShell スクリプトを %TEMP%\rust-setup.ps1 に書き、VBScript ランチャー経由で起動する。ここで WScript を噛ませているのは、Cargo のジョブオブジェクトから脱出するためだ。ビルドが終わってプロセスツリーが畳まれても、実装は生き残る。
条件分岐なし: フィーチャーフラグも環境変数チェックも一切ない。対応プラットフォームなら、毎回のビルドで無条件に走る。CI が5分おきにビルドしているなら、5分おきに実行される。
第2段のバイナリはアーキテクチャごとに配信名が分かれており、rust-crate_0.1.0(Linux x86_64)、0.2.0(Windows x86_64)、0.3.0(macOS x86_64)、0.4.0(macOS aarch64)という命名になっている。macOS の Intel / Apple Silicon を撃ち分けている点からも、開発者マシンが明確に標的だったことが読み取れる。
registry の安全機能を配送路に変える
この攻撃で最も応用が利く手口が、yank の転用だ。
攻撃者は 0.3.10 を公開した直後、arrayref の過去の安全なリリース(0.3.5〜0.3.9)をすべて yank した。
yank は本来、壊れたバージョンや脆弱なバージョンを「新規に選ばれないようにする」ための安全機能だ。既存の Cargo.lock は壊さないが、開発者には「使っているバージョンが yank されている」という警告が出る。その警告を見た開発者は何をするか。アップデートする。そして yank されていない選択肢は、その時点で 0.3.10 ただ1つだった。
StepSecurity の分析はこれを的確に言い当てている。
the attacker turned the registry’s own safety feature into the delivery channel (攻撃者は registry 自身の安全機能を、配送チャネルに変えた)
ここから導かれる、多くの人が誤解しがちな点が1つある。Cargo.lock があれば安全、ではない。効くのは「lock が既に 0.3.9 で固定されていて、その86分間に cargo update を打たなかった」場合だけだ。arrayref = "0.3" のようなキャレット指定は、解決時に 0.3.10 を掴む。CI で毎回依存を解決し直す構成、あるいは Dependabot / Renovate が yank 警告に反応して自動 PR を上げる構成なら、人間が判断する前に汚染版へ到達しうる。lock ファイルは解決結果の記録であって、解決の禁止ではない。
誰がやったのか — DPRK 系キャンペーンとの重なり
Wiz の分析は、インフラの重複から北朝鮮系の攻撃キャンペーンとの結びつきを指摘している。
第2段の実装が beacon する C2 エンドポイントは /49890878。このパスは、Microsoft が DPRK / Sapphire Sleet に帰属させた Mastra キャンペーンで使われていたものと同一だ。さらに配信元 IP 23.254.165[.]112 は、同じく Mastra に紐づく 23.254.167[.]13 と SSL 証明書の発行者(WIN-A6QF8AHPQH1\Administrator@WIN-A6QF8AHPQH1)を共有している。被害報告に出てきた 23.254.167[.]216 は、Mandiant / Google Cloud Threat Intelligence が UNC1069 による axios npm 攻撃の分析で挙げた IP だ。いずれも Hostwinds LLC の 23.254.164.0/23 レンジに収まっている。
第2段の実装が実際に何をするかも判明している。
- 認証情報の窃取: Chrome / Brave / Edge の SQLite ログインデータベースを直接クエリしてブラウザ保存の認証情報を抜く
- 永続化: Windows は Registry Run キー、macOS は LaunchAgent、Linux は systemd user service
- C2 通信: HTTPS POST でホスト情報と窃取した認証情報を Base64 エンコードした JSON として送信
- リモート実行:
runscriptを含む4つのコマンドをサポートし、PowerShell / shell スクリプトを同期・非同期で実行できる - フォールバック: 主 C2 に到達できない場合、DGA(ドメイン生成アルゴリズム)で5日ごとに10個の
.comドメインを生成する
「ビルドが1回通っただけ」の代償として得られるものとしては、十分すぎる。依存に Solana / Ethereum ランタイムが並んでいたことと、DPRK 系のオペレーションが暗号資産を主目的とすることを合わせると、絵はきれいに繋がる。
エンジニアは今日、何をすべきか
影響確認
まず、汚染版を掴んでいないかを確認する。Cargo.toml の指定ではなく、解決結果を見るのがポイントだ。
grep -rEn 'proc-macro1|proc-macro-en|aovine|arone|aronenao|tinymember|arrayref.*0\.3\.10|internment.*0\.8\.7|append-only-vec.*0\.1\.9' \
--include=Cargo.lock --include=Cargo.toml .
ローカルキャッシュに .crate ファイルが残っていないかも見る。
find ~/.cargo/registry/cache -type f \
\( -name 'arrayref-0.3.10.crate' -o -name 'internment-0.8.7.crate' \
-o -name 'append-only-vec-0.1.9.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
該当した場合、そのマシンはフル侵害として扱うのが妥当だ。ブラウザ保存の認証情報が抜かれている前提で、SSH 鍵・クラウドトークン・署名鍵をローテーションする。egress で 23.254.165.112 / 23.254.167.107 / 23.254.167.216 を全ポート遮断し、07:11〜09:25 UTC の窓でビルドした成果物はクリーンな環境で作り直してからデプロイする。
なお crates.io は悪意ある yank を巻き戻し済みなので、arrayref は 0.3.9 以前が警告なしの正常な選択肢に戻っている。念のため固定するなら arrayref = "=0.3.9"、internment = "=0.8.6"、append-only-vec = "=0.1.8"。
構造的な対策
個別の IoC を潰すのは対症療法にすぎない。同じ形の攻撃は必ずまた来る。効くのは以下の層だ。
ビルド時コード実行を持つエコシステム全体を棚卸しする。 これは Rust だけの話ではない。npm の postinstall、Python の setup.py、Dart / Flutter の build_runner、Gradle のビルドスクリプト。いずれも「依存を取得してビルドする」だけでコードが走る。npm なら --ignore-scripts を既定にする、といった単位で潰していける。
CI のビルドコンテナを使い捨てにし、egress を絞る。 永続化の手段が Registry Run キー / LaunchAgent / systemd user service である以上、毎回破棄されるコンテナでは足場が残らない。加えて、ビルドジョブから任意の IP への 443 が開いている状態は再考の余地がある。
CI ランナーに配る認証情報を短命・最小スコープにする。 開発者マシンとCIランナーは、cargo build を打つ瞬間だけ「未検証の第三者コードを自分の権限で実行する環境」になる。そこに長命の本番クレデンシャルを置かない。
依存の解決を人間のレビュー可能な事象にする。 cargo-deny / cargo-audit を CI に入れ、lock ファイルの差分を PR レビューの対象にする。今回のように yank が誘導に使われるケースでは、「警告が出たから上げる」を自動化していること自体がリスクになる。
長期的には、サンドボックス
Rust プロジェクト側も、この穴を認識していないわけではない。build script のサンドボックス化は Rust Project Goals に挙げられており、設定可能なサンドボックス環境を Cargo の unstable feature として入れるか、実験を速く回すため crates.io のサードパーティプラグインとして提供する方向で検討が進んでいる。安定化の際はまずオプトイン、次の Edition で既定オンにする、という段取りが想定されている。
ただし、これは簡単な問題ではない。build script に何を禁じるべきかの線引きが難しいからだ。ファイルシステムアクセスを全面的に奪えば、オブジェクトファイルを書く cc や、外部の文法仕様を読む pest が壊れる。「正当なビルドスクリプトがやること」と「マルウェアがやること」は、システムコールのレベルではかなり重なっている。
まとめ
arrayref0.3.10 は悪意あるコードを一行も含まず、タイポスクワットされたproc-macro1への依存行を1つ足しただけで、cargo buildによるリモートコード実行を成立させた- 攻撃面は
build.rs— ビルドを実行するユーザーの権限でコンパイル時に走るため、実行時サンドボックスは原理的に無力であり、本体コードの監査でも見つからない - 攻撃者は過去の安全版を yank して 0.3.10 を唯一の非 yank 版にした。registry の安全機能が配送路に転用されており、
Cargo.lockの存在は自動的な防御にはならない - C2 エンドポイントと IP レンジは DPRK 系の Mastra / UNC1069 キャンペーンと重なり、第2段はブラウザ認証情報の窃取と3プラットフォーム対応の永続化を行う
- 公開から削除まで86分。実被害はほぼ出ていないが、成立していたことに変わりはない
この事件の教訓は「Rust は危ない」ではない。依存を取得してビルドする行為自体が、未検証の第三者コードを自分の権限で実行する行為であるという、普段は意識しない事実が可視化されただけだ。Rust の build script サンドボックス化が Edition 単位の話である以上、当面は運用側で埋めるしかない。まずは自分の CI が、cargo build や npm install を「信頼済みの操作」として扱っていないかを見直すところからだ。
今日のその他のニュース
Mojo is now open source(HN 269pt) Modular の Mojo 言語がついにオープンソース化。MLIR ベースで CPU/GPU 双方をターゲットにする Python スーパーセットで、クローズド開発への批判が長く続いていた。コンパイラ本体が公開されたことで、Python の高速化レイヤーとしての採用検討が現実的な選択肢に入ってくる。
AliExpress runs silent WebAudio fingerprinting(HN 715pt) AliExpress のページが無音の WebAudio コンテキストを常時保持してフィンガープリンティングしており、その副作用で Bluetooth ヘッドホンのマルチポイント接続が切れるという報告。Web 側のトラッキング実装が OS レイヤーの体験を壊す実例で、「音を鳴らしていないのにオーディオデバイスが掴まれ続ける」挙動そのものが検出のヒントになる。
Git at any scale(HN 188pt) Cursor による大規模リポジトリでの Git 運用の技術記事。AI コーディングエージェントがリポジトリ全体を頻繁に読み書きする前提での Git 設計、という切り口が新しい。モノレポや巨大履歴を抱えているなら、操作コストの抑え方として読む価値がある。
ソース
- Supply chain attack on arrayref — Rust Blog
- Malicious Rust Crate arrayref Runs a Build-Time Payload — SafeDep
- Rust Supply Chain Attack on arrayref: Significant Overlap with DPRK Campaigns — Wiz
- Rust Supply-Chain Attack: arrayref 0.3.10 and the proc-macro1 Typosquat Execute a Remote Payload at Build Time — StepSecurity
- RUSTSEC-2026-0260 — RustSec Advisory Database
- Explore sandboxed build scripts — Rust Project Goals
- Sandbox/jail build scripts — rust-lang/cargo Issue #5720