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: исследовательская цепочка RCE в AutoGen Studio

Microsoft обнаружила локальную цепочку RCE через MCP в dev-сборке AutoGen Studio. Её исправили до релиза; в PyPI она не выходила.[1], [2]

💡 Простыми словами (ELI5)

Представьте исследовательскую лабораторию, где помощник читает записки посетителей. Его пускают в диспетчерскую, потому что он работает в том же здании. На входе для этой комнаты пропускают обычную проверку личности, а записка посетителя может выбрать запускаемый инструмент. AutoJack объединил эти упрощения: браузерный агент показал недоверенное содержимое, оно достигло локальной плоскости управления MCP в AutoGen Studio, а переданные вызывающей стороной настройки могли привести к запуску дочернего процесса. Нужно аутентифицировать плоскость управления, убрать параметры запуска из WebSocket URL и изолировать помощника от рабочей станции разработчика. Microsoft сообщает, что исправление появилось в ветке upstream до публикации PyPI. Исследование касается кода до выпуска, а не затронутой версии для клиентов.[1]

Ключевые понятия и термины

AutoJack
Название Microsoft для продемонстрированной цепочки в разрабатываемой версии AutoGen Studio. В отчёте сказано, что маршрут MCP исправили до включения в опубликованный пакет PyPI.[1], [2]
Loopback / localhost
Сетевой интерфейс для доступа к службам на том же хосте. Он ограничивает доступ извне, но не изолирует локальные процессы, например браузер агента, от других служб хоста.[1]
Проверка Origin для WebSocket
Серверная проверка заголовка Origin при WebSocket handshake. Microsoft установила, что в проверенном сценарии агента такая проверка сама по себе не отделяла отображённую страницу от локальной службы MCP.[1], [3]
StdioServerParams
Конфигурация MCP stdio в AutoGen: программа и аргументы для запуска процесса сервера. Описанный маршрут декодировал эти настройки из параметра URL WebSocket.[1], [2], [5]
Confused deputy
Привилегированный компонент побуждают использовать собственные полномочия в интересах недоверенной стороны. Здесь браузерный агент и локальная служба Studio связали содержимое страницы с запуском процесса на хосте.[1]

Связи со стандартами

Хронология инцидента

До 18.06.2026

Microsoft сообщила о поведении в MSRC; сопровождающие AutoGen усилили ветку upstream в коммите b047730. Согласно отчёту, затронутый маршрут MCP WebSocket ещё не публиковался в пакете PyPI.[1], [2]

18.06.2026

Microsoft опубликовала исследование AutoJack и меры защиты. В нём описана контролируемая локальная демонстрация, а не подтверждённая атака на клиентов.[1]

Пошаговый сценарий атаки

Шаг 1

Агенту поручают просмотреть недоверенное содержимое

Запрос, полученная страница или другой недоверенный ввод заставляет браузерный агент AutoGen отобразить страницу злоумышленника на той же машине, где работает AutoGen Studio.[1]

Шаг 2

Отображённая страница обращается к локальному MCP WebSocket

В испытанной Microsoft конфигурации скрипт страницы мог подключиться к конечной точке MCP Studio на localhost. Список Origin не изолировал плоскость управления от кода, показанного локальным браузером агента.[1], [3]

Шаг 3

Маршрут MCP обходит обычный middleware аутентификации

Затронутый middleware пропускал пути /api/mcp, предполагая, что проверку выполнит обработчик WebSocket. По данным Microsoft, обработчик этого не делал.[1], [4]

Шаг 4

Параметры сервера задаются в запросе WebSocket

Маршрут разработки декодировал server_params из URL в StdioServerParams. Затем транспорт MCP stdio использовал command и args для запуска дочернего процесса.[1], [2], [5]

Шаг 5

Цепочка достигает учётной записи хоста

Локальный процесс Studio запускает настроенный дочерний процесс с полномочиями своей операционной системы. Microsoft показала это в контролируемом локальном proof of concept и отмечает, что маршрут не входил в опубликованный пакет PyPI.[1], [2]

Исходный код: Уязвимый vs Защищённый вариант

Уязвимый пример разбирает параметры запуска, контролируемые вызывающей стороной, но намеренно не запускает процесс. Исправление upstream убрало настройки из запроса WebSocket, перенесло их в одноразовое серверное состояние сессии и восстановило аутентификацию маршрутов MCP. Этот защитный шаблон добавляет срок действия, ограничение размера, блокировку для параллельного доступа и список разрешённых исполняемых файлов; замените примерное имя точными разрешёнными бинарными файлами MCP. Он не заменяет проверку Origin и изоляцию ОС.[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 должен поступать из проверенного middleware, а не из данных запроса.
    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]

Чек-лист по защите системы для инженеров

Источники