ひとつのセッション、百のリポジトリ
Mandiant の AI Risk and Resilience 2026 レポートは、ある SaaS 事業者で稼働中の AI コーディングアシスタントのセッションを乗っ取り、それを通じて汚染された PyPI パッケージを入れさせ、GitHub の OAuth トークンを盗み、Shai-Hulud ワームを約 100 の内部リポジトリへ広げた攻撃者を描いている。レポートはセッションがどう奪われたかを書いていない。Bromure Agentic Coding のワークスペースなら書く必要もない。そのセッションにあるトークンは `ghp_` のダミーであり、外へ送ろうとすれば VM は一時停止し、一台を百のリポジトリに変える push はホストのプロキシで止まる。
誰かが、開発者自身のマシンで稼働し、認証済みで、信頼されていたコーディング セッションを奪った。そのあとに続いたことは、百ものリポジトリに至るまで、 すべてそのマシンがそのとき抱えていたものから出てきた。
ある SaaS 企業の開発者が、ありふれた問題にどのパッケージを使えばいいかを
コーディングアシスタントに尋ねた。アシスタントはひとつ挙げた。開発者は
はいと答えた。アシスタントを使う意味はまさにそこにある。あとは
pip install が片づけた。
そのときにはもう攻撃者がセッションを握っていて、その推奨を打ち込んだのは 攻撃者だった。
Mandiant はこの事例を AI Risk and Resilience Report 2026 に記している。今月、Google Cloud のインシデント対応部門が公表したものだ。 The Hacker News は 9 月 16 日に報じた。 攻撃者は開発者のワークステーション上で稼働中のコーディングアシスタント セッションを乗っ取った。そこから五つの段階を経て、約 100 の内部コード リポジトリと自社のパッケージのネームスペースに到達し、そこで二人目の従業員が 汚染された版を取得した。
連鎖を一段ずつ
Mandiant は、いちばん知りたい部分を伏せている。公開された事例研究は、侵入が いつ起きたかを書かず、攻撃者が稼働中のセッションをどう掌握したのかも書かない。 盗まれたブラウザの Cookie、悪意ある拡張機能、共有マシン上の汚染された CLI。 レポートはそのどれも選ばない。修正手段を求めて読む者は、手ぶらで帰ることになる。
段階 2 から 6 にはエクスプロイトがひとつもない。パッケージのインストールは
パッケージのインストールとして動き、開発者の権限で走るコードが開発者のディスクに
置かれたトークンを読む。有効なトークンを携えた git push は push し、有効な
公開権限を携えた公開は公開する。侵入だったのは一段階だけで、残りの五つは
支払いだった。
増幅器は資格情報であり、それは今も膨らみ続けている
Shai-Hulud は 2025 年からメンテナのアカウントを食い荒らしている自己複製型の
サプライチェーンワームで、この種の話にペイロードとして現れるのは、段階 5 こそ
作者たちがそれを作った目的だからだ。GitGuardian は 8 月に最近の亜種を解析し、
469 か所の資格情報の在処を漁っていることを突き止めた。
以前のビルドの 189 か所からの増加である。開発環境、CI/CD ツール、クラウドの
設定、AI ツールの設定、パッケージマネージャの設定ファイル、シェルの履歴、
.env ファイル、IDE の設定、CLI のキャッシュ。ワームは欠陥を狩るのではなく、
書類棚を読む。
GitGuardian の分析は、この類型を一文でこう言い切る。
「攻撃者は信頼関係を破ろうとするのをやめ、その信頼関係をすでに成り立たせて いる資格情報を使い始めた。」
一台のワークステーションが百のリポジトリになったのは、それが百のリポジトリ分の 権限を抱えていたからだ。あの数字は目録から出てきたものであり、目録に何を 入れるかを決めるのはあなたである。
そのセッションが抱えていたのはダミーだった
Bromure Agentic Coding は、あなたの Mac 上の Linux 仮想マシンの中でエージェントを 動かす。そしてそのマシンは本物の秘密をひとつも抱えない。そのために何かを 有効にする必要はない。ワークスペースが資格情報を保持するやり方が、そもそも そうなっている。
本物の GitHub トークンはホスト側で暗号化されたままだ。VM が受け取るのは
構造を保った偽物である。ghp_ に 36 文字が続き、全体で 40 文字。gh 自身の
接頭辞と長さの検査は文句なく通る。Bromure はそれを GH_TOKEN として
エクスポートし、~/.git-credentials と gh の設定に書き込む。そのマシンの中に
ある GitHub の資格情報はそれだけだ。ホスト上のプロキシは、要求が VM を出たあとに
本物の値を通信路上で差し替える。しかも差し替えるのは、宛先がその資格情報の
発行先ホストと一致するときだけだ。偽物はそれぞれ決定論的に、本物の値と
インストールごとの 32 バイトのソルトから HKDF-SHA256 で導出される。だから自分の
鍵の指紋を取るクライアントから見ても、セッションをまたいで鍵が入れ替わったように
は見えない。
そこに段階 4 をぶつけてみる。情報窃取プログラムが到着し、469 か所を漁り、環境
変数を見つけ、~/.git-credentials を見つけ、gh の設定を見つけ、見つけたものを
送り出す。読み取りはすべて成功し、ファイルはどれもワームが期待した場所にある。
攻撃者の手元に残るのは、どこでも認証されない 40 文字の文字列だ。ただ一台の
特定の Mac をふたたび経由する場合を除いて。
段階 5 も 6 も、段階 4 が使えるトークンを生んでいることを前提にしている。そして 段階 4 が生んだのはダミーだった。
窃取そのものが警報になる
偽のトークンには、正当な宛先がひとつしかない。github.com 向けに発行された
ghp_ のダミーを別のホストへ運ぶ無害な要求など存在しない。だからプロキシは
送信される要求のヘッダと本文を毎回走査し、自分の適用範囲から出ていく偽物を
探す。使うのは Aho-Corasick オートマトンで、全トラフィックに向けても十分に安い。
プロキシがそれを見つけたとき、単に記録するだけでは終わらない。
- HTTP 451 で要求を拒否し、1 バイトも宛先に届かない。
- Bromure はその場で VM を一時停止する。
- 警告が、シャットダウン、調査用に保存(フォレンジックのためにまず ディスク、ホーム、共有フォルダを書き出す)、または自己責任での続行を 提示する。
- Security Timeline に赤い Credential brokering の行が加わる。
- Bromure はそのワークスペースを侵害済みと記録し、次回の起動でディスクイメージと 永続ホームを消す。あなたのトークン、SSH 鍵、ワークスペース設定はその消去を 生き延びる。
これらを有効にする操作はいらない。検出器は常時動いている。段階 4 の持ち出しの 試み自体がセッションを止めるので、この事件は百のリポジトリと事例研究ではなく、 一時停止した VM とタイムラインの一行で終わる。
百のリポジトリとは百回の push である
念のためもう一枚重ねたいとしよう。段階 5 はワームがリポジトリに書き込むことで あり、ホストは書き込みを分類する。
ワークスペースのガードレールは、サービスごとに書き込み
ポリシーを持つ。GitHub については、このポリシーは REST API と同じように
git-over-HTTPS も読む。git push は git-receive-pack として届いて書き込みと
数えられ、git fetch は git-upload-pack として届いて読み取りと数えられる。
読み取り専用は push を拒む。新しいワークスペースの既定である書き込み前に
確認は、操作名を示すホスト側のダイアログのために push を一件ずつ止める。fetch は
どちらでも通るので、エージェントは作業を続けられる。
その判断を下すのは VM の外、ホスト上のプロキシだ。セッションが他人のものに なったエージェントには、ポリシーを切ることも迂回することもできない。ポリシーは エージェント側で動いていないからだ。百のリポジトリを狙うワームは、百回の拒否を 受け取るか、あなたに百回尋ねることになる。
そもそもパッケージは届かなければならなかった
段階 3 こそ、Mandiant が具体的な助言を出しているところだ。AI が推奨した サードパーティ依存を暗号学的チェックサムと許可リストに照らして検証すること、 そして依存の通信を自分が管理するリポジトリ経由にすること。
ワークスペースでは、パッケージの取得はすでにどれもホストを通っている。VM から
ネットワークへの経路はプロキシだけだからだ。Bromure は PyPI を pypi.org と
files.pythonhosted.org で捕まえ、そこにホストのポリシーを適用する。VM の中の
pip.conf は、プロキシが出したものをさらに絞ることはできるが、緩めることは
できない。Python パッケージについては三つの層が発言する。
- 経過日数のゲート。既定で有効、最短二日で、基準より新しい版を拒む。 公開されたばかりのリリースこそ、たった今乗っ取られた可能性がいちばん高い、 という考え方による。PEP 503 のインデックスには時刻が載らないので、Bromure は 公開時刻をその都度調べに行く。
- OSV チェック。
api.osv.devに対して行い、無料で鍵もいらない。あなたが 選んだ深刻度以上の勧告が出ている版をすべて遮断する。 - socket.dev の侵害パッケージフィルタ。マルウェア、既知のマルウェア、 タイポスクワッティング、悪質なインストールスクリプトに反応する。
遮断は HTTP 451 として返り、本文は
Bromure Supply-Chain Security blocked this request: で始まり理由が続く。pip は
それをそのまま表示する。なぜインストールが失敗したのかがあなたに見え、
エージェントにも見える。エージェントはたいてい自分で古い版を固定できる。その
古い版こそ、経過日数のゲートが向かわせようとしていたものだ。
Mandiant の三つの対策と、その置き場所
レポートは事例研究を三つの推奨で締めくくる。そのどれもが、Bromure の ワークスペースがすでに境界を置いている場所を名指ししている。
AI が推奨した依存を検証する
Mandiant は、アシスタントが勧めるものすべてにチェックサムと許可リストを 求める。あなたのワークスペースは、レジストリの応答に対して、VM がそれを 見る前にポリシーを適用する。プロキシは新しすぎる版をバージョン一覧から 取り除くので、エージェントから見ればそれらはまだ存在しない。新しすぎる版が 直接指定された場合は、パッケージの実際の日数をエラーに引いて拒否する。 評価サービスの鍵はホストに残り、インストールを走らせるマシンの外にある。
生の鍵と長命のトークンを近づけない
これは通信路の境界を助言の形で書いたものだ。VM の中のファイルも環境変数も プロセスも、本物の API キー、OAuth トークン、AWS のシークレット、SSH の 秘密鍵を持たない。Bromure はサブスクリプションの OAuth トークンをホスト側で 暗号化して保持し、失効の約五分前にそこで更新する。だからゲストは リフレッシュトークンを持ち歩かない。両者の間の経路は一方通行だ。ホストが 偽物を書き込み、VM の側には本物のトークンを返せと頼む呼び出しがない。
依存の通信を自分が管理するものに通す
プロキシは VM から外へ出る唯一の経路だ。HTTPS の要求はどれも仮想ソケットを 通ってホストへ渡り、ホストが TLS を終端し、要求を検査し、macOS 自身の TLS スタックで送り直す。この経路が一本しかないことが、ほかの二つの対策を努力 目標ではなく強制力のあるものにしている。パッケージの取得が検査を飛ばすことは できず、資格情報が差し替えなしに出ていくこともできない。
そして被害範囲を意図して小さく保つ
百のリポジトリが落ちたのは、一台のマシンが百のリポジトリ分の権限を 抱えていたからだ。ワークスペースはプロジェクトごと、あるいは資格情報の 境界ごとに切り分けよう。ひとつがひとつの VM に対応し、二つが同じ VM を 共有することはなく、それぞれがそのワークスペースの仕事に必要な資格情報だけを 持つ。一台のマシンの目録は設定画面で決められる。
修正パッチの当たらない部分
Mandiant はこの類型を、事件そのものよりその形を描く一文にまとめている。 「汚染されたデータソース、モデルの依存、あるいは拡張のフックは、信頼された エージェントを、内部偵察や横展開、あるいはサンドボックスからの自律的な脱出の 経路に変えうる。」
入口は三つ挙がり、文はひとつで、そのすべてに当てはまる。入口は攻撃者の 到着の仕方を決める。ある開発者のマシンの中身は、攻撃者がどこまで行けるかを 決める。今回の攻撃者は乗っ取られたセッションから入ってきた。次の攻撃者は別の どこかから入ってくるし、レポートはあなたが施錠すべき扉を何も名指ししない。
入口の次の段階のために設計しよう。セッションはすでに他人のものだと仮定する。 段階 1 を決して説明しないことで、この事例研究があなたに強いている仮定が まさにそれだ。そのうえで、そのセッションがどこまで届くのかを問う。
この事件のある版は段階 3 で終わる。端末に 451 が出て、Security Log に一行残る。 別の版は段階 4 で終わる。VM が一時停止し、資格情報が出ていこうとしたことを タイムラインの赤い行が記録する。どちらの版も段階 5 には届かない。そして百の リポジトリがあったのは段階 5 だった。
エージェントは、ダミーだけを抱えたマシンに置こう。 Bromure Agentic Coding を導入して、誰かにセッションを 取らせればいい。