2026年8月20日、crates.io サプライチェーン攻撃が発生した。Rustのパッケージレジストリ crates.io で、累計2億4,516万ダウンロードの基盤クレート arrayref を含む3本の正規クレートが、乗っ取られたメンテナアカウントから悪性バージョンごと公開された。Rust Security Response Team は同日中に全バージョンを削除し、公式ブログで「arrayref の作者が悪意を持って行動したとは考えていないが、そのコンピュータまたは資格情報が侵害された可能性が高い」と述べている。
この事件を「またサプライチェーン攻撃か」で済ませると、実務上いちばん重要な部分を見落とす。攻撃者は悪性版を公開しただけではなく、その39秒後に旧バージョン5本をまとめて yank し、依存解決の逃げ道を塞いでいた。この手口は公式ブログにもベンダーの分析にも書かれていないが、crates.io のレジストリindexは git リポジトリなので、すべての操作がコミットとして秒単位で残っている。本記事はその一次資料から時系列を再構成し、自分の環境での確認手順まで通しで整理する。
サプライチェーン攻撃の一般的な手口・防御の全体像は サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説 にまとめている。本記事はその中でも「レジストリの yank 機構が攻撃に転用された」個別事例として読んでほしい。
- ・何が起きた:正規クレート3本(
arrayref 0.3.10/internment 0.8.7/append-only-vec 0.1.9)が悪性依存ごと公開された。公開時間は86〜107分で、いずれも削除済み。 - ・手口の核心:悪性版のpublishから39秒後に旧版5本を一括 yank。
--precise 0.3.9でのロールバックを封じた(indexのgit履歴で確認)。 - ・実行経路:ペイロードは
build.rs。クレートを呼び出さなくても、ビルドが通るだけで実行される。 - ・影響範囲:
arrayrefへの直接依存宣言403本のうち398本が、レンジ上0.3.10を許容していた。 - ・スキャナでは出ない:確認時点(JST 8/20 22:44)で RustSec・GHSA とも当該advisory未登録。
cargo auditは反応しない。
Rustプロジェクトを持っているなら、まず本文「自分の環境を確認するコマンド」の
find を実行してほしい。汚染版はレジストリから削除済みなのでこれから新規に引き込まれることはないが、8月20日 UTC 07:15〜09:25 の間にビルドまたは cargo fetch を実行していた場合、ローカルキャッシュに実体が残っている可能性がある。crates.io サプライチェーン攻撃で何が起きたのか——3本の正規クレートと6本の攻撃用クレート
Rust公式ブログが削除を公表したのは、性質の異なる2グループ・計9クレートである。
グループ1:乗っ取られた正規クレート(3本)——いずれも droundy(David Roundy)名義で長年公開されてきた実在のクレートで、その最新版として悪性バージョンが上乗せされた。
| クレート | 悪性版 | 公開(UTC) | 削除(UTC) | 公開時間 | 累計DL | 直近90日DL | 安全な版 |
|---|---|---|---|---|---|---|---|
arrayref |
0.3.10 | 07:15:00 | 08:41:40 | 86分 | 245,163,990 | 53,684,091 | 0.3.9 |
internment |
0.8.7 | 07:34:07 | 09:04:11 | 90分 | 14,390,826 | 3,529,658 | 0.8.6 |
append-only-vec |
0.1.9 | 07:37:49 | 09:25:24 | 107分 | 4,488,423 | 1,348,774 | 0.1.8 |
グループ2:攻撃者自身が用意したクレート(6本)——proc-macro1・proc-macro-en・aovine・arone・aronenao・tinymember の全バージョンが削除された。このうちペイロードを持っていたのは proc-macro1 と、その後継として用意された proc-macro-en である。
proc-macro1 という名前は、Rustエコシステムで事実上必須の基盤クレート proc-macro2 を狙ったタイポスクワットだ。Cargo.toml の依存一覧に proc-macro1 が並んでいても、proc-macro2 の見間違いとして視線が滑る。攻撃者はこの視認性の低さを利用している。
proc-macro2 とは無関係本記事に出てくる
proc-macro1 / proc-macro-en は攻撃者が作成した別物であり、David Tolnay 氏がメンテナンスする正規クレート proc-macro2 には何の問題もない。proc-macro2 を使い続けて差し支えない。残る4本は事件当日より前から存在していた
aovine・arone・aronenao・tinymember の4本は、名前からそれぞれ append-only-vec・arrayref・internment(の別名候補)・tinyset を意識したものと読めるが、いずれもindex履歴上は 2026-08-19T08:15:39Z のindex収束コミット(Collapse index into one commit)の時点ですでに存在していた。つまり事件当日の08-20より前に登録されていたことは確実だが、収束コミットが過去の履歴を潰しているため、正確な作成日時はindexからは特定できない。「前日から仕込まれていた」と断定できるのはここまでで、それ以上の遡及は一次資料では裏が取れない。
同じアカウントの他クレートは改変されていない
droundy 名義のクレートには scaling と tinyset もあり、いずれも今回の被害クレートの依存グラフ内に登場する。両者についてindex履歴を確認したところ、8月19日の収束コミット以降、8月20日を通じて一切の変更コミットが無い。公式ブログの削除リストにも含まれていない。乗っ取りの範囲は3クレートに留まったと見てよい。
秒単位の時系列——publishの39秒後に旧版が一括yankされた
ここが本記事の主題である。crates.io のレジストリindexは rust-lang/crates.io-index という git リポジトリとして公開されており、publish・yank・un-yank・delete のすべてが1操作=1コミットとして記録される。汚染された .crate ファイル自体は削除済みで入手できないが、「いつ何が起きたか」はコミット履歴から完全に復元できる。
以下は ar/ra/arrayref 他のパスに対するコミットと、その差分を実際に読んで整理した時系列である(すべてUTC)。
なお前掲のクレート表がRust公式の発表値(レジストリへの publish 時刻)なのに対し、以下はindexへのコミット時刻である。index への書き込みはレジストリの処理から数秒遅れるため、arrayref は 07:15:00 と 07:15:02、append-only-vec は 07:37:49 と 07:38:22 のように両表で数値がずれる。矛盾ではなく、記録している地点が違う。
| 時刻 | 対象 | 操作 |
|---|---|---|
| 〜08-19 08:15 | aovine arone aronenao tinymember |
index収束コミット時点で既に存在 |
| 01:55:37 | proc-macro1 1.0.106 |
作成。依存は flate2 quote rayon rustversion tar unicode-ident——通信系ゼロ |
| 07:11:18 | proc-macro1 1.0.107 |
更新。base64 rustls ureq を追加=ここで武器化 |
| 07:15:02 | arrayref 0.3.10 |
公開。依存に proc-macro1 ^1.0.107 |
| 07:15:27 | arrayref 0.3.9 |
yank |
| 07:15:31 | arrayref 0.3.8 |
yank |
| 07:15:34 | arrayref 0.3.7 |
yank |
| 07:15:38 | arrayref 0.3.6 |
yank |
| 07:15:41 | arrayref 0.3.5 |
yank(公開から39秒で旧版5本を掃討) |
| 07:34:13 | internment 0.8.7 |
公開。依存に proc-macro1 |
| 07:38:12 | internment 0.8.3〜0.8.6 |
4本を一括 yank |
| 07:38:22 | append-only-vec 0.1.9 |
公開と 0.1.7・0.1.8 の yank を単一コミットで同時実行 |
| 08:03:09 | proc-macro1 |
crates.io が削除(ペイロード供給元が断たれる) |
| 08:31:33 | proc-macro-en 1.0.10 |
作成。依存構成は proc-macro1 1.0.107 と完全一致 |
| 08:41:40 | arrayref 0.3.10 |
crates.io が削除 |
| 08:51:29 | proc-macro-en |
crates.io が削除 |
| 09:04:11 | internment 0.8.7 |
削除 |
| 09:04:28〜09:06:17 | aovine arone aronenao tinymember |
順次削除 |
| 09:15:53〜09:16:59 | arrayref 0.3.5〜0.3.9 |
un-yank(復旧) |
| 09:22:36〜09:22:41 | append-only-vec 0.1.7・0.1.8 |
un-yank |
| 09:25:24 | append-only-vec 0.1.9 |
削除(最後の悪性版が消える) |
| 09:26:20〜09:26:28 | internment 0.8.3〜0.8.6 |
un-yank(復旧完了) |
この表から、公式発表やベンダー分析には現れない3つの事実が読み取れる。
事実1:1.0.106 と 1.0.107 の差分が「武器化の瞬間」を示している
proc-macro1 は 01:55:37 に 1.0.106 として先に登録されている。この版の依存は flate2・quote・rayon・rustversion・tar・unicode-ident で、HTTP通信を行うクレートが1つも含まれていない。約5時間後の 07:11:18 に公開された 1.0.107 で、base64・rustls・ureq の3本が追加された。ureq はHTTPクライアント、rustls はTLS実装、base64 はエンコーダである。
Aikido Security は悪性コードの挙動として「プラットフォーム別のペイロードをダウンロードする」「通信先をbase64で隠している」と報告しているが、その通信スタックが依存として追加された瞬間が 07:11:18 だったことは、index のメタデータだけで独立に確認できる。1.0.106 は通信能力を持たない「足場」で、1.0.107 が武器である。
そして arrayref 0.3.10 の公開はその わずか3分44秒後(07:15:02)。攻撃者は武器を置いてから即座に配送に移っている。
事実2:旧版の一括yankが塞いだのは「退避経路」だった
Cargo における yank は、本来「このバージョンは使わないでほしい」とメンテナが表明するための機構だ。yank されたバージョンは新規の依存解決の候補から外れるが、既存の Cargo.lock に記録済みなら引き続き使える。
攻撃者はこれを反転させて使った。arrayref 0.3.10 を公開した直後、0.3.9 → 0.3.8 → 0.3.7 → 0.3.6 → 0.3.5 の順に、3〜4秒間隔で5本を yank している。等間隔であることからスクリプト実行と分かる。
ここで注意したいのは、この yank は「悪性版を選ばせる」ためのものではないという点だ。Cargo は要求レンジ内で最新のバージョンを選ぶので、arrayref = "0.3.9"(=^0.3.9)は 0.3.10 が公開された 07:15:02 の時点で、yank の有無に関わらず 0.3.10 に解決される。後述のフェーズ表でも、yank 前(B)と yank 後(C)で着地先は 398本のまま変わっていない。
yank が実際に奪ったのは退避と防御の手段である。
・ロールバックの封鎖——異常に気づいた開発者が最初に打つ cargo update -p arrayref --precise 0.3.9 は、yank 済みのバージョンを指すため通らない。0.3.8 以下へ下げようとしても同じで、0.3.4 まで落とさないと逃げられない状態が約2時間続いた
・完全固定していた依存の破壊——=0.3.5 でバージョンを固定していた4本は、レンジ上 0.3.10 を受け付けない唯一の構造的な免疫を持っていた。0.3.5 が yank されたことで、この4本は「悪性版は掴まないがビルドは通らない」状態に落ちた
crates.io 側の復旧手順もこの読みと整合する。悪性版 0.3.10 の削除(08:41:40)だけでは復旧は完了せず、その34分後に 0.3.5〜0.3.9 の un-yank(09:15:53〜09:16:59)が独立した工程として実施されている。悪性版の除去と、利用者の退避経路の回復は、別々に手当てが必要だったということだ。
append-only-vec では手口が少し違い、0.1.9 の公開と 0.1.7・0.1.8 の yank が単一のindexコミット(07:38:22)に同居している。internment では公開(07:34:13)と4本の一括yank(07:38:12)が約4分空いている。3クレートで操作の粒度が揃っていないのは、攻撃の途中で手順を調整していた形跡と読める(ここは推測であり、断定できる材料は無い)。
arrayref = 0.3.9"] --> B{"解決候補は?"} B -->|"平常時"| C["0.3.9 を採用
正常"] B -->|"07:15:27 以降
旧版は全て yank"| D["候補は 0.3.10 のみ"] D --> E["proc-macro1 1.0.107
を依存として取得"] E --> F["build.rs が実行される"] F --> G["外部からペイロード取得
(Aikido報告)"] style D fill:#fee2e2,stroke:#dc2626 style F fill:#fee2e2,stroke:#dc2626 style G fill:#dc2626,color:#fff
事実3:08:41以降は「解決できない」状態が34分続いた
crates.io が arrayref 0.3.10 を削除したのは 08:41:40、旧版の un-yank が始まったのは 09:15:53 である。この約34分間、0.3.10 は存在せず、0.3.5〜0.3.9 は yank されたままだった。
攻撃者が yank したのは 0.3.5 以降の5本だけなので、0.3.0〜0.3.4 は生き残っている。したがってこの時間帯の依存解決は「解決不能」か「9年前の 0.3.4 へ静かに巻き戻る」かのどちらかになった。後述の影響範囲マトリクスで内訳を示す。
なぜビルドしただけで感染するのか——build.rsの実行モデル
Rust Blog は悪性コードについて「proc-macro1 にはペイロードをダウンロードするビルドスクリプトがあった」とだけ記している。この一文が意味するところが、この事件の危険度を決めている。
Cargo の build.rs は、そのクレートをコンパイルする前にビルドマシン上でネイティブ実行されるRustプログラムだ。本来はCライブラリのリンク設定やコード生成のために用意された仕組みで、依存クレートの build.rs も同様に実行される。
つまり、arrayref の関数を1行も呼んでいなくても、Cargo.toml の依存グラフに入っていれば十分である。cargo build はもちろん、cargo check・cargo test・cargo clippy・rust-analyzer によるIDEのバックグラウンド解析でも build.rs は動く。エディタでプロジェクトを開いただけ、というケースが成立しうる。
npmの postinstall に相当する経路だが、決定的な違いがある。npm には --ignore-scripts があり、npm v12ではpostinstallがデフォルト無効化される方向に進んでいる。一方 Cargo には build.rs の実行を一括で止める標準オプションが無い。build.rs はビルドの正常な一部として設計されているためだ。
| 観点 | npm | Cargo(Rust) |
|---|---|---|
| 実行される仕組み | preinstall / postinstall |
build.rs(ビルドスクリプト) |
| 実行タイミング | パッケージ取得・インストール時 | 依存クレートのコンパイル時 |
| 呼び出し不要で動くか | 動く | 動く |
| 一括無効化オプション | --ignore-scripts あり |
標準オプション無し |
| IDE解析で動くか | 動かない | 動く(rust-analyzer) |
| 既定での無効化 | v12で前進 | 予定なし |
今回の悪性コードは
arrayref 本体ではなく、arrayref が依存する proc-macro1 の build.rs にあった。つまり間接依存の、さらにビルド時実行部分である。cargo tree で依存を展開しない限り、Cargo.toml を眺めているだけでは存在に気づけない。Aikido Security の分析によれば、build.rs はプラットフォーム別(Linux x86_64・Windows x86_64・macOS x86_64・macOS aarch64)のバイナリを取得し、Unix系では実行権限を付けてデタッチして起動、Windowsでは wscript/VBS 経由でPowerShellを起動してCargoのjob objectから脱出する設計だったという。報告されている通信先は 23.254.165.112 の 9089番ポート(ペイロード配信)と443番ポート(C2)、Unix側の設置先は /tmp/rust-setup である。
これらの挙動とIOCは Aikido Security の報告に基づくもので、本記事では独立検証していない——汚染された .crate ファイルはcrates.ioから削除済み(static.crates.io は該当ファイルに403を返す)で、第三者が実体を取得して再現する手段が現時点で無いためだ。公式ブログ側の記述が「ペイロードをダウンロードするビルドスクリプト」という最小限に留まっているのに対し、ベンダー分析のみが詳細を持つ、という情報の非対称がこの事件の現状である。
自分の環境を確認するコマンド
汚染版はすべてcrates.ioから削除されたため、これから新規に引き込まれることはない。確認すべきなのは「2026年8月20日 UTC 07:15〜09:25 の間に取得してしまっていないか」の一点である。
手順1:Rust公式が案内するキャッシュ検査
Rust Blog が案内しているのは、~/.cargo/registry/cache 配下に該当する .crate ファイルが無いかを直接探す方法だ。レジストリから削除されてもローカルキャッシュの実体は消えないため、この検査が最も確実である。
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
何も出力されなければ、そのマシンは汚染版に触れていない。 展開済みソースも併せて確認したい場合は、src 側も同じ条件で検査する。
find ~/.cargo/registry/src -maxdepth 2 -type d \( \
-name 'arrayref-0.3.10' -o -name 'internment-0.8.7' -o \
-name 'append-only-vec-0.1.9' -o -name 'proc-macro1-*' -o \
-name 'proc-macro-en-*' \) -print
手順2:lockfileとvendorディレクトリを検査する
キャッシュを消しても、リポジトリにコミットされた Cargo.lock や cargo vendor の成果物には汚染版が記録されたままになりうる。プロジェクトのルートで実行する。
# lockfile に悪性版が固定されていないか(全リポジトリを横断)
grep -rn -E 'name = "(arrayref|internment|append-only-vec|proc-macro1|proc-macro-en)"' \
--include=Cargo.lock -A1 . \
| grep -E -B1 'version = "(0\.3\.10|0\.8\.7|0\.1\.9|1\.0\.10[67]?)"'
# vendor ディレクトリに実体が残っていないか
find . -type d -path '*/vendor/*' \( -name 'arrayref' -o -name 'proc-macro1' -o \
-name 'proc-macro-en' -o -name 'internment' -o -name 'append-only-vec' \) -print
手順3:依存グラフのどこから引き込まれているかを特定する
自分の Cargo.toml に arrayref を書いていなくても、間接依存で入っていることの方が多い。cargo tree の逆引き(-i)で経路を出す。
cargo tree -i arrayref
cargo tree -i internment
cargo tree -i append-only-vec
# ビルド時にだけ効く依存を洗い出す(build.rs 経路の可視化)
cargo tree -e build --prefix depth | grep -E 'proc-macro1|proc-macro-en'
手順4:安全な状態へ寄せる
汚染版は削除済みなので、通常は cargo update で自動的に健全なバージョンへ寄る。明示的に上げる場合は次のとおり。
cargo update -p arrayref --precise 0.3.9
cargo update -p internment --precise 0.8.6
cargo update -p append-only-vec --precise 0.1.8
# 念のため取得し直す(キャッシュ由来の実体を排除したい場合)
cargo clean && cargo fetch --locked
手順1で1件でもヒットした場合、そのマシン上でビルドが実行されたかどうかに関わらず、扱っていた資格情報を侵害前提で扱うのが安全側の判断になる。SSH秘密鍵、クラウドのアクセスキー、CI/CDトークン、
~/.cargo/credentials.toml のレジストリトークン、ブラウザ保存の認証情報が対象。特にCIランナーで検出された場合は、そのランナーが参照できるシークレット全件のローテーションを検討する。CIキャッシュは見落としやすい
GitHub Actions の actions/cache や Swatinem/rust-cache を使っている場合、~/.cargo/registry がキャッシュキーごと保存されている。ローカルを掃除してもCI側のキャッシュから汚染版が復活しうるので、該当期間のキャッシュエントリは削除しておく。
# 該当リポジトリのRust関連キャッシュを一覧・削除(gh CLI)
gh cache list --limit 100 | grep -i cargo
gh cache delete <CACHE_KEY>
自動更新ボットを運用している場合の考え方は RenovateとDependabotのサプライチェーン攻撃対策 にまとめている。今回のように「最新版が悪性」というケースでは、自動マージの設定が被害を拡大させる方向に働く。
影響範囲マトリクス——403の依存宣言はどう解決されたか
arrayref に直接依存を宣言しているクレートは、crates.io API の reverse_dependencies で 403本である。ここで重要なのは、「403本が被害を受けた」ではなく「403本の依存宣言のうち、何本がレンジ上 0.3.10 を許容していたか」という問いだ。
Cargo のキャレット要件では ^0.3.9 は >=0.3.9, <0.4.0 を意味するため、0.3.10 は 0.3.9 より新しい正当な候補として一致する。403本すべての要求レンジを Cargo の仕様どおりに評価した結果は次のとおり。
| 分類 | 本数 | 内訳 |
|---|---|---|
0.3.10 を許容するレンジ |
398本 | ^0.3 ^0.3.5〜^0.3.9 ~0.3 ~0.3.6 >=0.3 ^0 |
0.3.10 を許容しないレンジ |
5本 | =0.3.5 固定4本(holochain系3本+lib3h_persistence_api)、^0.2 1本(onionsalt) |
403本中398本、98.8%が悪性版を受け入れるレンジだった。 例外はバージョンを完全固定していた4本と、そもそも 0.2 系に留まっている1本だけである。キャレット要件はRustの標準的な書き方なので、これは「書き方が悪かった」という話ではなく、マイナー0系クレートのパッチ更新は原理的に自動採用されるという設計上の帰結だ。
フェーズ別に「新規解決がどこへ着地したか」
さらに、時系列の各フェーズで403本の依存宣言が新規解決したときにどのバージョンへ着地したかを、index履歴から復元した可用性(publish済み・yank状態・削除済み)に基づいて計算した。
| フェーズ | 時刻(UTC) | 悪性版 0.3.10 に着地 | 悪性版以外に着地 | 解決不能=ビルド失敗 |
|---|---|---|---|---|
| A 平常時 | 〜07:15:02 | — | 403本 | 0本 |
| B 悪性版公開直後 | 07:15:02〜07:15:27 | 398本 | 5本 | 0本 |
| C1 旧版5本をyank後 | 07:15:27〜08:03:09 | 398本 | 1本 | 4本 |
| C2 proc-macro1削除後 | 08:03:09〜08:41:40 | 選択されるが解決不能 | 1本 | — |
| D 0.3.10削除〜un-yank前 | 08:41:40〜09:15:53 | — | 86本(うち85本は0.3.4へ巻き戻り) | 317本 |
| E 復旧後 | 09:15:53〜 | — | 403本 | 0本 |
読み取れることが2つある。
実際にペイロードまで到達しえたのはフェーズC1の約48分間である。新規解決を行ったプロジェクトの98.8%が 0.3.10 に着地し、その依存 proc-macro1 も取得できたのはこの区間だけだ。
crates.io が proc-macro1 を削除した 08:03:09 以降(C2・約38分)は事情が変わる。arrayref 0.3.10 自体はまだindexに残っているが、その依存 proc-macro1 ^1.0.107 はindexから消えている。依存グラフを完成させられないため、この時点から新規の依存解決でペイロードに到達する経路は断たれた(internment・append-only-vec も同じ proc-macro1 に依存していたので、3クレートすべてで同時に鎖が切れている)。
つまり公式が公表した「オンライン時間」86分/90分/107分と、「新規解決でペイロードまで届いた時間」は別の指標である。後者は arrayref で約48分、internment で約29分、append-only-vec で約25分になる。
鎖が切れたのは新規の依存解決の話に限られる。08:03:09 より前に一度でも
proc-macro1 を取得していれば、その実体は ~/.cargo/registry/cache に残り、それ以降も何度でもビルドに使われる。Cargo.lock に記録済みのプロジェクト、vendorディレクトリ、CIキャッシュも同じで、レジストリ側の削除は既に手元へ落ちたものには一切効かない。公式の復旧案内が lockfile の確認ではなく find ~/.cargo/registry/cache だったのは、まさにこのためである。フェーズD(34分間)は別種の事故を起こしている。 0.3.10 が削除され旧版がまだ yank されたままのこの時間帯、317本は依存解決に失敗してビルドが落ち、残る86本のうち85本は要求レンジが緩かったために 2017年4月公開の 0.3.4 へ静かに巻き戻った。CIが赤くなったチームは異常に気づけたが、^0.3 のような緩い指定で 0.3.4 に落ちたケースはビルドが通ってしまうため気づきにくい。この時間帯に生成された Cargo.lock が残っていないか、確認する価値がある。
# フェーズDの巻き戻りが残っていないか(9年前の版を掴んでいないか)
grep -A1 'name = "arrayref"' Cargo.lock | grep 'version = "0\.3\.[0-4]"'
cargo auditで検出できない理由とスキャナ対応表
サプライチェーン事故が起きたとき最初に打つのは cargo audit だが、今回はこれが反応しない。
cargo audit が参照するのは RustSec Advisory Database である。本記事の確認時点(JST 2026-08-20 22:44 / UTC 13:44)で、当該事件のadvisoryは登録されていない。 リポジトリ内の crates/internment/ に存在するのは RUSTSEC-2020-0017 と RUSTSEC-2021-0036 という無関係な既存advisoryのみで、arrayref・append-only-vec のディレクトリ自体が存在しない。advisory-db の最新コミットは 2026-08-18 で、事件当日のコミットは入っていない。
GitHub Advisory Database(GHSA)も同様で、ecosystem=rust の直近公開分に該当エントリは無く、arrayref・append-only-vec への影響を宣言したadvisoryはゼロ、internment にあるのは2021年の既存2件のみである。GHSAが空ということは、Dependabot も反応しないことを意味する。
| 検査手段 | 参照先 | 今回検出できるか |
|---|---|---|
cargo audit |
RustSec Advisory DB | ❌ advisory未登録 |
cargo deny check advisories |
RustSec Advisory DB | ❌ 同上 |
| Dependabot alerts | GitHub Advisory DB | ❌ GHSA未登録 |
cargo update |
crates.io(削除済み) | ⭕ 健全版へ寄る(事後) |
公式 find コマンド |
ローカルキャッシュの実体 | ⭕ 現時点で唯一確実 |
cargo tree -i |
依存グラフ | ⭕ 経路の特定に有効 |
これは advisory-db や GHSA の不備というより、「削除で対処された事件」はadvisoryが立ちにくいという構造的な性質だ。advisoryは通常「この版を避けよ」と伝えるための仕組みだが、当該版がレジストリから消えていれば新規流入は止まっているため、優先度が下がる。しかしすでにローカルへ落ちた実体には何の効果も無い。だからこそ公式ブログが案内したのが、advisoryを引くコマンドではなく find によるキャッシュの直接検査だったのだと読める。
「レジストリから削除された悪性パッケージ」は、多くのスキャナの視野の外に落ちる。npm・PyPI・crates.io を横断した事例は Trapdoorキャンペーンの記事でも同じ構図を扱っている。削除は新規感染を止める措置であって、既感染を検知する措置ではない。
Cargo.lock運用の落とし穴と恒久的な防御
今回の事件は、Rustの依存管理でよく語られる「ベストプラクティス」のうち、どれが効いてどれが効かなかったかをはっきり示した。
効いた防御:lockfileのコミット
Cargo.lock をリポジトリにコミットし、CIで --locked を付けてビルドしていたプロジェクトは、フェーズB・Cを無傷で通過している。yank は新規解決の候補から外す操作であって、既存の Cargo.lock を無効化しないためだ。lockfileに arrayref 0.3.9 と書かれていれば、0.3.9 が yank されていようとそのまま使われる。
ライブラリクレートでは Cargo.lock をコミットしない慣習が長く一般的だったが、バイナリ・アプリケーション・CIを持つプロジェクトではコミットするのが今回のような事故に対する最も安価な防御になる。
# CIで再解決を禁止する(lockfileと不一致なら失敗させる)
cargo build --locked
cargo test --locked
効かなかった防御:cargo update の定期実行と自動マージ
依存を新しく保つ運用は、通常はセキュリティ上の善である。しかし「最新版が悪性」という今回の形では、更新頻度がそのまま被曝確率になる。86分の窓に cargo update が当たったプロジェクトは、それが自動化されているほど確実に踏んだ。
現実的な緩和は、更新を止めることではなく遅延させることだ。npm/pnpm には公開直後のバージョンを一定期間避ける cooldown の考え方があるが、Cargo に同等の標準機構は無い。運用でカバーするなら、依存更新PRの自動マージを外し、最低でも数日は寝かせるポリシーにする。今回の悪性版はいずれも2時間以内に消えているので、この遅延だけで全て回避できた計算になる。
効かなかった防御:advisoryベースのスキャン
前章のとおり cargo audit も Dependabot も沈黙する。advisoryは事後・かつ登録されたものしか見えないという前提で、防御の主軸には置かないほうがよい。
検討に値する追加の防御
・cargo vendor で依存を固定化する——cargo vendor した成果物をリポジトリに置けば、レジストリの状態変化から完全に切り離される。差分レビューの対象にもなるので、build.rs の追加は目に見える形になる
・cargo tree -e build を定期的に見る——ビルド時実行される依存だけを列挙する。今回のような「間接依存の build.rs」は、この視点でしか浮かび上がらない
・依存追加時に build.rs の有無を確認する——新規依存を入れるときだけでも、build.rs を持つクレートかどうかを見る習慣は費用対効果が高い
・CIをネットワーク制限下で走らせる——ビルド中の外向き通信を既定で遮断すれば、build.rs からのペイロード取得は失敗する。今回のペイロード取得は素の外部IPへの直接接続だったため、egress制御があれば止まっていた
メンテナ側から見た教訓
Rust公式は「作者のコンピュータまたは資格情報が侵害された可能性が高い」とし、crates.ioのアカウントを予防的にロックしたと述べている。侵入経路は現時点で公表されていないため、フィッシングかトークン流出かマシン侵害かを断定する材料は無い。
なお本記事の確認時点で、github.com/droundy は 404 を返す(リダイレクトは無く、リポジトリ単位ではなくアカウント全体)。一方 crates.io 側では droundy が依然として3クレートの所有者として表示される。GitHubは削除と一時停止のどちらも公開上は 404 として扱うため、この404の原因を外部から判別することはできない。公式が言及しているのは crates.io アカウントのロックであり、GitHubアカウントの状態については何も述べていない。両者を同一視しないほうがよい。
crates.io は2FAを必須化しているが、publishに使うAPIトークンは2FAの外側で機能する。トークンをCIやローカルの設定ファイルに置いたままにしないこと、スコープと有効期限を絞ること、公開作業をTrusted Publishing(OIDC)へ寄せていくことが、メンテナ側の実務的な対策になる。
まとめ——crates.io サプライチェーン攻撃から持ち帰るもの
2026年8月20日のcrates.io侵害は、公開時間が86〜107分と短く、Rust Security Response Team の対応も速かった。だが「短かったから軽微」ではない。arrayref は直近90日で5,368万回ダウンロードされる基盤クレートであり、その窓に新規解決が当たれば98.8%の依存宣言が悪性版に着地する状態だった。
技術的な要点を3つに絞ると、次のようになる。
・yank は防御機構だが、旧版に一括適用すると攻撃に使える。 悪性版のpublishから39秒で旧版5本が消え、逃げ道が塞がれた。この操作はindexのgit履歴にしか残っていない
・build.rs は「使っていなくても動く」。 間接依存のビルドスクリプトなので、Cargo.toml を読んでも気づけない。Cargoには --ignore-scripts に相当する標準の停止手段が無い
・削除で対処された事件はスキャナに映らない。 advisory未登録のため cargo audit も Dependabot も沈黙する。確認できるのはローカルキャッシュの実体だけ
そして運用上の結論は単純だ。Cargo.lock をコミットして --locked でビルドし、依存更新は自動マージせず数日寝かせる。 この2つだけで、今回の86分の窓は完全に回避できた。
同種の攻撃は今後も繰り返される。レジストリの削除対応は速くなっているが、それは新規流入を止める措置であって、すでに手元に落ちたものを消してはくれない。事件の一報を見たら、まず find ~/.cargo/registry/cache を叩く——その反射を持っておくのが、いちばん実効性のある備えになる。
参照ソース
- Rust Blog — Supply chain attack on arrayref(公式・削除バージョンと公開時間の一次情報)
- rust-lang/crates.io-index — arrayref のindexコミット履歴(本記事の秒単位タイムラインの一次資料)
- crates.io API — arrayref のバージョン・所有者・ダウンロード数
- crates.io — arrayref の逆依存一覧(403本の依存宣言の出典)
- Aikido Security — Two popular Rust crates compromised in supply chain attack(ペイロード挙動とIOCの出典)
- The Cargo Book — Dependency Resolution(キャレット要件・yankの仕様)
- The Cargo Book — Build Scripts(build.rs の実行モデル)
- RustSec Advisory Database(確認時点で当該advisory未登録)