すべての投稿に戻る
公開日 · 著者 Renaud Deraison

コミットはブランチだった

Air は 9 月 17 日に Plugin4Shell を公開した。Claude Code、Codex、Gemini CLI、Copilot のプラグインマーケットプレイスは、各プラグインを 40 文字のコミットハッシュに固定する。ところがエージェントは、そのハッシュを本当に取得できたのかを一度も確かめずにチェックアウトする。固定されたハッシュと同じ名前のブランチを作った攻撃者がチェックアウトを勝ち取り、あとはバックグラウンドの更新処理が、確認も操作もないまま仕上げてしまう。Anthropic と OpenAI は修正した。Google は修正しないと答え、Microsoft はまだ出していない。Bromure Agentic Coding のワークスペースでは、チェックアウトを勝ち取ったコードが降り立つのは、ダミーの認証情報しか持たない VM の中であり、その先にはホスト側で適用される push ポリシーと egress ルールがある。

サプライチェーンの手引きはどれも、依存関係はハッシュに固定せよと説く。バージョン 番号は誰かが動かせるラベルにすぎないが、ハッシュは内容そのものだからだ。四つの コーディングエージェントはその助言に従った。そして、受け取ったものが本当にその ハッシュだったかを、どれ一つ確かめなかった。

プラグインがレビューを通る。あなたが読むか、プラットフォームチームが読むかして、 コミットに固定された形で承認リストに載る。40 文字の十六進数は、木を変えれば文字も 変わるのだから、ただ一つの木しか指し示せない。そこが魅力なのだ。適切な権限さえ あれば誰でもタグを動かしたりバージョンを再公開したりできるが、コミットハッシュは 事実であるはずだった。

9 月 17 日、Air の研究者三人(Or Nevo、Dor Granat、Niv Hoffman)が Plugin4Shell を公開した。彼らの 結論はこうだ。固定は、それを解決する側の質を超えられない。そして主要な四つの コーディングエージェントの解決処理は、自分の仕事を検算していない。 The Register も同日に報じた。 Claude Code、OpenAI の Codex、Google の Gemini CLI、Microsoft の Copilot がいずれも 影響を受ける。

git は ref を優先する

コミットハッシュは内容であり、同時に名前でもある。ブランチもタグもそうであり、 git はそのすべてを同じ探索で解決する。ブランチ名でもありオブジェクト ID でもある 文字列を git に渡せば、git は選ばざるをえない。git はブランチを選ぶ。研究者は一行 でこう言う。「ある名前が有効な ref であると同時にオブジェクト ID でもあるとき、git は ref を優先する」。

git はちゃんと警告する。その参照について曖昧さの通知を出す。ただし、あなたが読んで いない端末に、である。チェックアウトを走らせているのはバックグラウンドの更新処理 だからだ。

git のブランチ名は 40 文字の十六進数を受け付ける。プラグインのリポジトリを握る攻撃 者は、マーケットプレイスが固定したのと同じ名前のブランチを作り、それを既定ブランチ にする。クローンは、同名のコミットと並べてそのブランチも持ってくる。そこでエージェ ントがチェックアウトを実行する。

git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

チェックアウトはブランチに着地する。Gemini CLI は別の道をたどって同じ場所に着く。 --ref で正しいコミットを取得したうえで git checkout FETCH_HEAD を実行するのだが、 リポジトリに FETCH_HEAD という名前のブランチがあれば、そちらに解決されてしまう。

一つの名前、二つの意味マーケットの固定値aaaaaaaa…aaaaレビュー済み、記録済み、不変リポジトリ内のオブジェクト IDaaaaaaaa…aaaaレビューを通過したコード同じ 40 文字のブランチrefs/heads/aaaaaaaa…aaaa攻撃者が push したものgit checkout aaaa…aaaaブランチに解決される警告は出るが誰も読まないエージェントは実際に何をチェックアウトしたのかを決して尋ねない。コマンド一つで済む: git rev-parse HEAD。
一つの文字列、二つの意味。コミット aaa…aaa と、aaa…aaa という名前のブランチが、クローンされたリポジトリの中に両方とも存在する。git checkout はどちらかを選ばねばならず、ref を選ぶ。マーケットプレイスが記録している固定値は同じままで、ディスク上の木は別人のものになっている。

五つの段階、そして最後の一つにクリックはない

研究者は攻撃を五つの動きに分解している。最初の三つは、よく運営されたマーケット プレイスが意図してやっていることだ。

仕込み。 無害なプラグインを公開する。コミット aaa…aaa でレビューを通る。 実際どこにも問題がないからだ。

普及。 開発者がそれを、レビュー済みコミットに固定してインストールする。手引きが 言うとおりに振る舞う段階である。

バージョン更新。 マーケットプレイスが、より新しいコミット bbb…bbb へ固定を 張り替える。こちらも無害で、こちらもレビュー済みだ。

ラグプル。 攻撃者が bbb…bbb という名前のブランチを作り、既定ブランチにして、 悪意あるコードを指すようにする。何も再公開せず、マーケットプレイスの記録も動かない。 あなたが確かめるであろう 40 文字は、すでに確かめたあの 40 文字のままだ。

自動更新から RCE へ。 Claude Code と Codex は既定で、インストール済みプラグイン をバックグラウンドで更新する。次の定期更新がブランチをチェックアウトする。Air は この段階を「確認もなく、クリックもない」と表現している。

Plugin4Shell、一段階ずつ誰の行動か1無害なプラグインが公開され、コミット aaa…aaa でレビューを通るこの時点でコードに問題は何もないマーケット2開発者がレビュー済みコミットに固定してインストールするこれが推奨されるやり方であり、正しく守られているあなた3マーケットプレイスがより新しいコミット bbb…bbb へ張り替えるこちらも無害、こちらもレビュー済みマーケット4bbb…bbb という名前のブランチが作られ、既定にされる何も再公開されず、記録された固定値も動かない攻撃者5バックグラウンドの更新処理がブランチをチェックアウトする開発者のマシン上でコードが実行される — 確認もなく、クリックもないエージェント
段階一から三は、マーケットプレイスが設計どおりに動いている姿であり、段階二は開発者が標準的な助言に従っている姿である。攻撃は段階四と五であって、そのどちらも開発者に何かを承認させたりしない。

仕込みを飛ばす二つめの入口もある。正規のプラグイン作者のリポジトリを乗っ取り、その プラグインをすでにインストールしている全エージェントへ悪意ある版を送り込むのだ。 ブランチの仕掛けは同じで、通すべきレビューは存在しない。

Air はその結果を「従業員のマシン上での完全なリモートコード実行」と表現する。その 従業員の権限で動く。攻撃者に権限昇格は要らない。コードはノートパソコンの持ち主 その人として動き、その人のファイルと本番アクセスにまで届く。

この研究者たちが同じ形を数え上げるのは、これで三度目になる。悪意あるエージェント スキルに関する以前の仕事では、26,000 を超えるエージェントに届いた一つと、すでに 使われていた 925 のスキルを 134,000 のエージェント上で乗っ取ったキャンペーンが 描かれている。あの手のキャンペーンは、あなたが悪いものをインストールする必要が あった。 今回は、あなたが良いものをインストールする必要があった。 そして待つだけでよかった。

四つのうち二つには修正がない

Air は 6 月に四社すべてへ報告した。

Anthropic は 6 月 17 日、バージョン 2.1.179 で Claude Code を修正した。OpenAI の 修正は Codex 0.146.0 で出荷され、8 月 12 日に検証された。Google は 8 月 4 日、 Gemini CLI は非推奨であり修正しないと回答した。Google は利用者を Antigravity へ 誘導しているので、ディスクに残り続ける Gemini CLI は脆弱なままである。Microsoft は Copilot 向けに何も出していない。GitHub は、自社プラットフォームがコミット SHA に 見えるブランチ名とタグ名を拒否すると述べている。それは GitHub 上のプラグインは 守るが、Bitbucket を含め別の場所でホストされたプラグインリポジトリには何もしない。 The Register は Microsoft 自身の数字を引いて、Fortune 500 のほぼ 90% が Copilot を 使っていると指摘している。

修正そのものは一行で、しかもエージェントの内側で走らねばならない。

test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort

作業ツリーに着地したコミットを解決し、一致しないかぎり先へ進むことを拒む。これを マーケットプレイスがあなたの代わりにやることはできない。固定を解決するのは、 マーケットプレイスが話し終えたあとに、あなたのマシンの上で動くあなたのエージェント だからだ。

今日これを読んでいる多くの開発者にとって、「エージェントを更新せよ」は取りうる答え ではない。そこで残るのがもう一つの問いだ。チェックアウトを勝ち取ったコードが走り 出したとき、そこには何があるのか。

Bromure Agentic Coding のワークスペースの中では

従業員の権限で動く、という Air の言い方は正確だ。ワークスペースは、それを外れに するために存在している。

Bromure Agentic Coding では、エージェントは Mac 上のハードウェア 仮想化された Linux VM の中で動き、その VM が本物の認証情報を持つことは一度もない。 セッション開始時に、アプリは構造を保ったダミーをエージェントの実行環境へ書き込む。 ANTHROPIC_API_KEYGH_TOKENLINEAR_API_KEY などを環境変数として、そして 対応する偽物を ~/.git-credentials~/.docker/config.json~/.kube/config~/.aws/config とエージェントの MCP 設定ファイルへ。どのダミーもツールが期待する 形を保っているので、ツールは受け入れる。GitHub のダミーは ghp_ に 36 文字、 Anthropic のものは sk-ant-api03-brm- で始まり、Kubernetes のベアラートークンは brm-k8s- で始まる。Bromure はそれぞれを、インストールごとのソルトを使った HKDF-SHA256 で本物の値から導く。おかげでセッションをまたいで安定し、拾った者に とっては無価値である。

本物の値はホスト側で暗号化されて置かれている。ホスト側のプロキシが、要求が VM を 出たあとに回線上でそれを差し替える。しかも、宛先がその認証情報の発行先ホストと 一致するときだけだ。この種のペイロードが真っ先にやること、つまり設定ファイルを 求めてホームディレクトリをさらう動きは、形の整ったダミー一式を拾うだけに終わる。

チェックアウトの後: 同じ三つの動き、二度ふつうの開発者マシンホームディレクトリを読むsk-ant-api03-… ghp_… AKIA…本物の鍵、本物のトークン、本物の kubeconfig成功届く範囲のリポジトリへ push するgit push → git-receive-pack開発者自身の git 認証情報成功集めたものをどこかへ送るPOST https://attacker.exampleふつうの外向き HTTPS、遮るものなし成功Bromure Agentic Coding のワークスペースホームディレクトリを読むsk-ant-api03-brm-… ghp_… brm-k8s-…ダミー。本物はホスト側にあるダミー届く範囲のリポジトリへ push するgit-receive-pack = 書き込み遮断、またはホストの確認ダイアログポリシー集めたものをどこかへ送る451 — 1 バイトも転送しないegress 規則、そして VM は一時停止停止
同じチェックアウト、同じ悪意あるプラグイン、異なる二台。ふつうのノートパソコンでは、コードは開発者の本物の認証情報を受け継ぎ、開発者の届く先すべてに届く。ワークスペースでは、読み取れる認証情報はダミーであり、試みた push はホスト側の書き込みポリシーに突き当たり、外部への呼び出しはゲストが編集できない egress ルールに突き当たる。

push はホスト側で承認されねばならない

侵害されたプラグインが開発者のマシン上でなしうる最も価値ある行為は push である。 一台のノートパソコンが百のリポジトリになる のはそうやってだ。

Guardrails は、エージェントが GitHub へ投げる呼び出しを一つずつ分類する。HTTPS 越し の git push はプロキシに git-receive-pack として届き、これは書き込みに数えられる。 読み取り専用モードはそれを拒む。書き込み前に確認モードは、あなたの Mac に Allow write on "<scope>" from workspace "<name>"? と題したダイアログを出し、操作の 要約ではなく操作そのものをそのまま見せる。git fetch は読み取りに数えられ、あなたを 邪魔せず通る。

分類はホストのプロキシ、つまり VM の外側で走る。ゲストの中のコードはそれを切ることも 回り込むこともできない。判断を下しているマシンの上で動いていないからだ。

外部への呼び出しは、ゲストが編集できない規則に突き当たる

どのワークスペースにも egress ファイアウォールがある。ホスト、IP 範囲、プロトコル、 ポート、そしてウェブ通信については個々の HTTP 動詞にまで及ぶ、pf 形式の許可と拒否の 順序付きリストだ。allow web api.example.com GET,POST は、VM の中のどのツールが要求 を出そうとも、回線のところで、エージェントに変更を伴わない問い合わせだけを許す。

これを二つの層が適用する。仮想ネットワークスイッチは、宛先 IP と DNS から読み取った ホスト名によって、各フローに規則を適用する。HTTPS だけでなく素の TCP と UDP も 覆う。プロキシは TLS のサーバー名と HTTP メソッドで、もう一度適用する。Bromure は VM のポート 80 と 443 の通信を、ゲストの協力なしにプロキシへ迂回させるので、内側の 何一つとして検査から逃れられない。あなたの編集は動作中のセッションへ即座に届く。 これはこういう情報公開を読んだ当日に効いてくる。

その下に侵害検出器が座っている。既定で有効、設定するものは何もない。プロキシは外向き の要求それぞれを一度の走査にかけ、そのワークスペース向けに発行されたダミートークンを 探す。発行先でないホストへ宛てられたダミーは持ち出しの兆候であり、プロキシは それに HTTP 451 で答え、1 バイトも転送しない。その場で VM を一時停止し、ワークスペース の停止、調査のためのディスクとホームフォルダの書き出し、あるいは自己責任での続行を 提示する警告を上げる。Bromure はそのワークスペースを侵害済みと記録し、次回の起動で VM のディスクとホームフォルダを消去する。あなたのトークン、SSH 鍵、ワークスペース 設定は残る。出ていったのはダミーだけなので、入れ替えるものは何もない。

ゼロクリックとは、気づくべきものが何もないということ

段階五のもっとも鋭い性質は、その静けさにある。バックグラウンドのプラグイン更新は 確認も出さず、ツール呼び出しも起こさない。だからエージェント自身の画面は、開発者に 捕まえるべきものを何も差し出さない。Air の指摘は、この攻撃があなたにとって見張る 理由のない部分で走る、ということだ。

ワークスペースは、その見張りを別の場所へ移す。セキュリティエンジンは判断の一つ ひとつを、あなたの Mac の Security Timeline ウィンドウに一行として書き出す。 ファイアウォールの判定(evil.example:443 tcp — blocked)、Guardrails の実施 (DELETE api.github.com/repos/… — blocked)、赤い blocked — exfiltration attempt, VM paused の行を含む credential brokering、さらに サプライチェーンの判定とプロンプトインジェクションの検知が、色分けされ絞り込める形で 並ぶ。セッションは、実行されたコマンドと到達したホストの暗号化された記録を残すことも できる。

黙って自分を更新したプラグインも、要求を出さないわけにはいかない。そしてその一つ ひとつが、ハイパーバイザーの向こう側で自分の記録をつけている境界を越えていく。

まず自分のエージェントのバージョンを確かめる

Claude Code 2.1.179 と Codex 0.146.0 には修正が入っている。Gemini CLI を使って いるなら、修正は出ない。Copilot を使っているなら、まだ出ていない。

できるところではプラグインの自動更新を切る

ゼロクリックという性質は、バックグラウンドの更新処理から来ている。手動更新は、 段階五に人間を戻してくれる。

CI でチェックアウトを自分で検証する

固定したチェックアウトのあとに git rev-parse HEAD を取り、固定値と突き合わせる。 ベンダーが追加せざるをえなかったのと同じ一行の確認であり、SHA でクローンする どんな場所でも使える。

誰かがチェックアウトを勝ち取る前提で設計する

固定もレビューもマーケットプレイスの審査も、ここではすべて機能していた。それでも 攻撃は通った。その先の結末は、勝ち取ったコードが何に届けるかで決まる。

Plugin4Shell は、慎重な開発者を不注意な開発者と同じ条件で捕らえる。流行っているもの をそのまま入れた人は、ソースを読み、ハッシュを固定し、その固定値を書き留めた人より 悪い目に遭ったわけではない。コンテンツアドレッシングは、その二人目を守るはずのもの だった。それが守らなかったのは、git が住所を、同じ文字列を別のものにも使う探索に よって解決するからである。

固定は続けよう。そのうえで、探索や名前の一致に支えられた制御はいつか、あなたが 眠っているあいだに誤った答えを手渡されるものと考えよう。その朝のために設計する こと。持ち去る価値のある鍵を一つも持たない場所でエージェントを動かし、それが何を したかの記録は、コードの手の届かない境界の向こう側に置いておくこと。