開発者のデスクトップで、何の操作もしていないのに calc.exe が立ち上がる。きっかけは、自分が動かしているAIエージェントに「このページを見て」と頼んだことだけ。しかも電卓を起動したのはブラウザではなく、エージェントのホストプロセス本体だった。
Microsoftのセキュリティチームが6月18日に公開した「AutoJack」は、この一見地味なデモにAIエージェント時代の急所が凝縮されている。狙われたのは AutoGen Studio、Microsoft Researchのマルチエージェント・フレームワーク AutoGen を GUI から触るためのプロトタイピングツールだ。エージェントを組み立て、MCP(Model Context Protocol)サーバーなどのツールを繋ぎ、手元で実験する。研究者の手すさびのための環境であって、本番運用を想定したものではない。そこが今回の話の入口になる。
なぜ「localhostは安全」が崩れるのか
ローカルで動くサービスは、伝統的に 127.0.0.1 からのアクセスだけを信用してきた。外から直接叩けないので、認証を緩めに作っても実害が出にくい。多くの開発ツールがこの前提に寄りかかっている。
ところが、Webをブラウズできるエージェントを同じマシンで走らせると話が変わる。Microsoftの言い方が的確だ。
エージェントがオープンなWebをブラウズしつつ、特権を持つローカルサービスとも通信できるようになった時点で、localhostは信頼境界ではなくなる。
エージェントが攻撃者のページを開くと、その中のJavaScriptは「localhost上のプロセスから出たリクエスト」として振る舞える。外部の悪意あるコードが、内部の信頼された経路に化けて入り込む。これがAutoJackの本質で、特定の1バグというより設計思想のズレを突いている。
3つの穴が直列につながる
AutoJackは単独では無害な3つの欠陥を鎖のようにつなぐ。標的はすべて AutoGen Studio の MCP WebSocket 周りだ。
| 連鎖 | 弱点 | 何が起きるか |
|---|---|---|
| ① 出自チェック | localhost を無条件に許可(CWE-1385) | エージェントのヘッドレスブラウザが localhost を名乗り通過 |
| ② 認証 | MCPパスを認証から除外(CWE-306) | トークン検証が誰も実行されず素通り |
| ③ 実行 | URLパラメータをそのまま起動(CWE-78) | 任意の実行ファイルを起動できる |
まず①。WebSocketはこう設定されていた。
allowed_origins = ["http://127.0.0.1", "http://localhost"]
通常のブラウザからの外部アクセスを弾くつもりの設定だが、エージェントが操るブラウザは同じマシン上で動く。Originヘッダは当然 localhost になり、許可リストを満たしてしまう。
②はもっと素朴なミスだ。認証ミドルウェアが、MCP系のパスを明示的にスキップしていた。
# これらのパスは認証を除外(各ハンドラ側で検証する想定だった)
if request.url.path.startswith("/api/ws") or request.url.path.startswith("/api/mcp"):
return await call_next(request)
「ハンドラ側でチェックするはず」という想定で除外したものの、当のWebSocketハンドラはそのチェックを実装していなかった。認証モードを none / github / msal / firebase のどれにしても、この口だけは開きっぱなしになる。責任の押し付け合いが生んだ典型的な穴で、コードレビューでいちばん見落とされやすい種類のものだ。
③が止めになる。エンドポイントは server_params というクエリ引数を base64 デコードし、中身をそのまま StdioServerParams として起動する。
encoded = websocket.query_params.get("server_params")
decoded = base64.b64decode(encoded)
params = StdioServerParams(**json.loads(decoded))
await create_mcp_session(bridge, params, session_id)
実行ファイルの許可リストは無い。つまり攻撃者は起動したいコマンドをJSONで渡すだけでいい。
{
"type": "StdioServerParams",
"command": "calc.exe",
"args": [],
"env": { "pwned": "true" }
}
攻撃の流れはこうなる。悪意あるページのJavaScriptが ws://localhost:8081/api/mcp/ws/?server_params=<base64ペイロード> へWebSocket接続を張る。開発者がエージェントにそのページを見るよう指示する。エージェントのブラウザがJSを実行し、①で出自チェックを抜け、②で認証を抜け、③でペイロードのコマンドが開発者の権限で走る。ユーザーの操作はURLを渡すことだけ、というのがこの攻撃の怖さだ。calc.exe がデモで使われているのは無害だからで、ここを powershell.exe や bash に置き換えればファイル窃取でもマルウェア常駐でも同じ要領でできる。
pip で入れた人は対象外、という重要な但し書き
ここは正確に押さえたい。煽り記事になりがちな話だが、影響範囲は限定的だ。脆弱なMCP WebSocketの実装(autogenstudio/web/routes/mcp.py)は、PyPIで配布された版には一度も含まれていない。pip install autogenstudio で導入した利用者は、このチェーンの影響を受けない。危ないのは GitHub の main ブランチを、該当機能が入ってから修正前までの期間にビルドして使っていたケースに絞られる。
裏返すと、「研究プロトタイプを最新ソースから追って動かす」開発者ほど踏みやすい構図でもある。最先端を触る人ほどリスクに近い、というのはエージェント開発全般に通じる皮肉だ。
修正は「URLに鍵を載せない」設計に変えた
修正コミット b047730 は、根本のデータフローを組み替えた。パラメータをURLに載せるのをやめ、サーバー側に持たせる方式にしている。POST /ws/connect でパラメータをサーバー内の pending_session_params に登録し、返ってくるのは session_id だけ。WebSocketが繋がった瞬間にその場でパラメータをpop(取り出して削除)する。コミットのコメントが意図を端的に語っている。
これにより、攻撃者がWebSocketのクエリ文字列経由で任意の server_params を注入することを防ぐ。
未知のセッションIDで来た接続はクローズコード 4004 で拒否される。あわせて認証の除外リストも絞り、/api/mcp はもうスキップされない。
興味深いのは、同じコミットがもう一つ別のRCE経路も塞いでいる点だ。FunctionTool の _from_config() がユーザー提供のソースコードを exec() で実行していたため、設定を流し込むだけでコードが走る穴になっていた。修正後はインスタンス化を行わず、スキーマ検証だけで済ませる。AutoJackの調査が、近くにあった別の地雷も掘り当てた格好だ。CVE番号は今のところ振られていない。
持ち帰るべきは「自分のマシンの設計」
このバグ自体はもう直っている。だが Microsoft が強調したのは、もっと射程の長い話だ。
このパターン(マシン上のエージェントがlocalhostのサービスに到達する)は、この1つのバグより広い問題だ。
ブラウズも実行もできるエージェントをローカルで走らせるなら、AutoGen Studio に限らず同じ罠が待っている。手元のマシンには、認証の薄いローカルサービスがいくつも動いているはずだ。開発サーバー、デバッグポート、各種MCPサーバー。それらは「外から来ない」前提で守られてきたが、その前提を内側から崩す存在をいま自分で招き入れている。
実務でできる対策はそれほど突飛ではない。エージェントのブラウズ用IDと開発者本人のIDを分ける(別のOSユーザー、コンテナ、VMに押し込む)こと。信頼できないコンテンツを処理するエージェントを、特権サービスと同じホストで走らせないこと。ローカルサービスはループバックだけにバインドし、ファイアウォールで非ループバックを塞ぐこと。MCPの実行ファイルには許可リストを設けること。要するに、エージェントを「自分と同じ権限を持つ同居人」ではなく「信頼区分の違う外部プロセス」として扱い直す、という発想の転換だ。
AIエージェントのセキュリティ議論はプロンプトインジェクションに偏りがちだが、AutoJackが見せたのは、注入されたプロンプトの先にある実行環境そのものが甘いと、結局はホストごと持っていかれるということだ。エージェントに能力を足すたびに、自分の信頼境界がどこへ動いたかを描き直す習慣を持っておきたい。