FlawOpen

flawopen.com/Incidents/microsoft-autogen-studio-autojack-agent-rce-june-2026

● Microsoft AutoGen project · CWE-1385 / CWE-306 / CWE-78 · 2026-06-18 · 高
セキュリティ研究 · FlawOpen

AutoJack:AutoGen StudioのRCE研究チェーン

MicrosoftはAutoGen Studio開発ビルドでローカルMCP経由のRCE研究連鎖を報告。公開前に修正され、PyPIには含まれませんでした。[1], [2]

💡 わかりやすい解説 (ELI5)

訪問者のメモを読むアシスタントがいる研究室を想像してください。同じ建物で働いているため、アシスタントは制御室に入れます。受付はその部屋だけ通常の本人確認を省き、訪問者のメモが起動する道具まで選べます。AutoJack はこの抜け道をつなぎました。ブラウジングエージェントが信頼できない内容を表示し、それが AutoGen Studio のローカル MCP 制御面に届き、呼び出し側の設定から子プロセスが起動し得ます。制御面を認証し、起動設定を WebSocket URL から外し、開発者の端末からエージェントを隔離してください。Microsoft は PyPI リリース前に upstream ブランチで修正したと説明しています。この調査対象はリリース前のソースコードであり、顧客向けの影響バージョンではありません。[1]

主要な概念と専門用語

AutoJack
AutoGen Studio の開発版で実証された攻撃連鎖に Microsoft が付けた名称です。報告によると、該当する MCP ルートは PyPI 公開パッケージに入る前に修正されました。[1], [2]
Loopback / localhost
同じホスト上のサービスへ接続するネットワークインターフェースです。外部ネットワークからの到達を制限しますが、エージェントのブラウザーなどのローカルプロセスを同一ホストのサービスから隔離するものではありません。[1]
WebSocket Origin 検証
WebSocket ハンドシェイク時にサーバーが Origin ヘッダーを確認する制御です。Microsoft のテストでは、この検証だけではエージェントが表示したページからローカル MCP サービスへの到達を防げませんでした。[1], [3]
StdioServerParams
AutoGen の MCP stdio 設定で、サーバープロセスの起動に使うプログラムと引数を含みます。報告されたルートは WebSocket URL のパラメーターからこれをデコードしていました。[1], [2], [5]
Confused deputy
特権を持つコンポーネントに、自身の権限を信頼できない相手のために使わせる状態です。ここではブラウジングエージェントとローカル Studio が、ページ内容からホスト上のプロセス起動へつながる経路になりました。[1]

フレームワーク対応

インシデントのタイムライン

2026-06-18 より前

Microsoft は MSRC に挙動を報告し、AutoGen のメンテナーはコミット b047730 で upstream ブランチを強化しました。報告では、該当する MCP WebSocket ルートは PyPI パッケージに未収録でした。[1], [2]

2026-06-18

Microsoft が AutoJack の調査結果と緩和策を公開しました。説明されているのは制御されたローカル実証であり、顧客への攻撃が確認されたという報告ではありません。[1]

ステップ・バイ・ステップの攻撃フロー

ステップ 1

エージェントが信頼できないコンテンツを閲覧する

プロンプト、取得したページ、または別の信頼できない入力により、AutoGen のブラウジングエージェントが AutoGen Studio と同じ端末上で攻撃者のページを表示します。[1]

ステップ 2

表示ページがローカル MCP WebSocket に到達する

Microsoft が試した構成では、ページのスクリプトが localhost 上の Studio MCP エンドポイントへ接続できました。Origin の許可リストは、ローカルエージェントブラウザーが表示したコードから制御面を分離できませんでした。[1], [3]

ステップ 3

MCP ルートが通常の認証ミドルウェアを迂回する

該当ミドルウェアは WebSocket ハンドラーが認証すると想定し、/api/mcp のパスを除外していました。Microsoft によると、そのハンドラーには認証確認がありませんでした。[1], [4]

ステップ 4

WebSocket クエリがサーバー設定を指定する

開発ルートは URL の server_params を StdioServerParams にデコードしました。その後、MCP stdio トランスポートが command と args を使って子プロセスを起動します。[1], [2], [5]

ステップ 5

ホストのユーザー権限に到達する

ローカルの Studio プロセスが、自身の OS 権限で設定された子プロセスを起動します。Microsoft は制御されたローカル PoC で実証し、該当ルートは公開済み PyPI パッケージにはなかったと説明しています。[1], [2]

ソースコード比較:脆弱 vs 堅牢化

脆弱な例は呼び出し側が制御する起動設定を解析しますが、意図的にプロセスは起動しません。upstream の修正では、設定を WebSocket クエリから外してサーバー側の一回限りのセッション状態に保存し、MCP ルートの認証も再び有効にしました。この防御パターンでは、有効期限、サイズ上限、並行アクセス用ロック、実行ファイル許可リストも加えています。例の名前は承認済み MCP バイナリに置き換えてください。Origin 検証や OS 隔離の代わりにはなりません。[1], [2]

脆弱な実装
import base64
import json
from dataclasses import dataclass


@dataclass(frozen=True)
class StdioServerParams:
    command: str
    args: tuple[str, ...]


def params_from_websocket_query(query: dict[str, str]) -> StdioServerParams:
    # 開発用ルートは URL で渡された設定を信頼していた。
    encoded = query["server_params"]
    decoded = base64.b64decode(encoded)
    return StdioServerParams(**json.loads(decoded))
堅牢化されたセキュアパッチ
from dataclasses import dataclass
from threading import Lock
from time import monotonic
from uuid import uuid4


@dataclass(frozen=True)
class StdioServerParams:
    command: str
    args: tuple[str, ...]


SESSION_TTL = 30.0
MAX_PENDING = 256
ALLOWED_COMMANDS = {"approved-mcp-server"}
pending: dict[str, tuple[float, StdioServerParams]] = {}
pending_lock = Lock()


def register_params(params: StdioServerParams, authenticated: bool) -> str:
    # authenticated は検証済みミドルウェアから取得し、リクエストデータから取得してはならない。
    if not authenticated:
        raise PermissionError("MCP control-plane authentication required")
    if params.command not in ALLOWED_COMMANDS:
        raise ValueError("unapproved MCP server executable")
    now = monotonic()
    with pending_lock:
        for key, (expires, _) in list(pending.items()):
            if expires <= now:
                pending.pop(key, None)
        if len(pending) >= MAX_PENDING:
            raise RuntimeError("pending MCP session limit reached")
        session_id = str(uuid4())
        pending[session_id] = (now + SESSION_TTL, params)
        return session_id


def consume_params(session_id: str, authenticated: bool) -> StdioServerParams:
    if not authenticated:
        raise PermissionError("MCP control-plane authentication required")
    with pending_lock:
        entry = pending.pop(session_id, None)
    if entry is None or entry[0] <= monotonic():
        raise ValueError("unknown or expired MCP session")
    return entry[1]

エンジニアリング&システム堅牢化チェックリスト

参考資料