Назад к блогу

Безопасность AI-агентов в браузере: как вредоносные расширения перехватывают управление ассистентами

Безопасность AI-агентов в браузере: как вредоносные расширения перехватывают управление ассистентами

Встроенные в браузеры ИИ-ассистенты всё чаще получают право не только читать страницы, но и действовать на них от имени пользователя — и это превращает обычное вредоносное расширение в инструмент полного захвата агента. Разбираем технику BragJack: какие разрешения ей нужны, как через отладчик и подмену сетевых запросов перехватывается управление ассистентом и почему для защиты конечных точек это принципиально новая категория угроз.

Встроенные в браузеры AI-ассистенты получают всё больше привилегий: они читают страницы, а в агентных браузерах — ещё и действуют на сайтах от имени пользователя. Атакующая техника BragJack показывает, что одного вредоносного расширения достаточно, чтобы перехватить управление таким ассистентом и воспользоваться его правами. Разбираем, как устроена эта цепочка на уровне браузерных API: какие разрешения нужны расширению, как оно подключается к цели через отладчик, внедряет код и синхронизируется с загрузкой страницы агента.

Чем BragJack отличается от классических атак расширений

BragJack — техника атаки, при которой одно вредоносное расширение браузера перехватывает управление встроенными AI-ассистентами. Атака требует, чтобы расширение уже было установлено; после этого она работает без взаимодействия с пользователем: расширение получает контроль над AI-агентом браузера и злоупотребляет его существующими привилегиями, чтобы добраться до чувствительных данных или действовать от имени жертвы.

Ключевой сдвиг — переход из категории «чтение данных» в категорию «управление AI-агентом». Агенты в таких браузерах, как Perplexity Comet и Opera Neon, могут действовать на сайтах, а не только читать их. Полученный доступ включал историю браузера, скриншоты, локальные файлы и возможность отправлять агенту инструкции. В случае Comet демонстрировалось принуждение агента посетить Perplexity, суммировать электронные письма жертвы и отправить результаты на другой адрес.

Для захвата применяется техника Prompt Forcing. В отличие от обычной prompt injection, где атакующий пытается вставить инструкции в читаемый контент, Prompt Forcing позволяет передать агенту целый промпт и последующие инструкции. Агент затем переводит эти инструкции в легитимные действия браузера, используя свои существующие привилегии.

По описанию исследователя, атакующий через расширение получает доступ, который может читать локальные файлы, достигать веб-контента, делать скриншоты и потенциально получать доступ к камере и микрофону браузера. В случае Chrome ослабление security-заголовков и перенаправление JavaScript-ресурса позволили выполнить код внутри контекста Gemini и напрямую связаться с привилегированным AI-компонентом Chrome, минуя обычный поток запросов Gemini. Всё это делается через declarativeNetRequest.

Для endpoint-защиты это означает, что конечное действие выполняет не обычный вредоносный код, а легитимное ПО, которому дают указание совершить атаку. Финальное действие исходит от доверенного приложения. По мере того как браузеры и другие endpoint-приложения получают более способных ИИ-агентов, скомпрометированное расширение, которое раньше видело только веб-контент, в некоторых архитектурах становится путём к ПО, читающему файлы, данные браузинга и действующему на сайтах от имени пользователя.

Модель угроз и необходимые разрешения

Возможности расширения определяются разрешениями. host permissions открывают доступ к части API: без них недоступны fetch() из service worker, чтение url, title и favIconUrl через browser.tabs, программная инъекция content script, webRequest, cookies и declarativeNetRequest. При этом host permissions не требуются для content script, объявленного в манифесте, для chrome.tabs при наличии разрешения tabs и для chrome.debugger.

Какие именно страницы затрагивает статически объявленный content script, задаёт обязательное поле matches — массив шаблонов. Дополнительно exclude_matches исключает страницы, include_globs добавляет фильтр по glob после matches, а exclude_globs исключает по glob. Скрипт внедряется, если URL совпадает с matches и include_globs и не совпадает с exclude_matches или exclude_globs. Для file:// URL и режима инкогнито пользователь должен отдельно выдать доступ на странице деталей расширения; проверить это можно через extension.isAllowedIncognitoAccess() или extension.isAllowedFileSchemeAccess().

На корпоративных устройствах политики могут блокировать подключение отладчика по модели «всё или ничего» в момент вызова browser.debugger.attach(). Если политика ExtensionSettings настраивает заблокированные хосты (runtime_blocked_hosts) для расширения, attach() блокируется на всех целях с ошибкой "Host access is restricted by policy." — даже если отдельные источники входят в runtime_allowed_hosts. Если политика DisableScreenshots отключает захват скриншотов или к цели применяются правила Data Loss Prevention (DLP), attach() завершается ошибкой "Screenshot capture is restricted by policy.".

Механика перехвата через chrome.debugger

Отладочный API Chrome — это альтернативный транспорт для протокола удалённой отладки Chrome. Через него расширение подключается к вкладкам, чтобы инструментировать сеть, отлаживать JavaScript, изменять DOM и CSS. Для использования требуется объявить разрешение "debugger" в манифесте. В целях безопасности API не даёт доступ ко всем доменам CDP, а только к перечисленному набору: Accessibility, Audits, CacheStorage, Console, CSS, Database, Debugger, DOM, DOMDebugger, DOMSnapshot, Emulation, Fetch, IO, Input, Inspector, Log, Network, Overlay, Page, Performance, Runtime, Storage, Target, Tracing, WebAudio и WebAuthn.

Цепочка начинается с chrome.debugger.attach(target, requiredVersion), где target — цель отладки. Затем вызывается chrome.debugger.sendCommand(target, method, commandParams?): target — сессия отладки, method — строка с именем метода, который должен быть одним из методов, определённых протоколом удалённой отладки, а commandParams — необязательный JSON-объект с параметрами, соответствующий схеме параметров протокола для данного метода. sendCommand отправляет заданную команду цели отладки и возвращает тело ответа; при ошибке отправки сообщения промис отклоняется. Порядок такой: attach() устанавливает соединение с указанной целью, после чего sendCommand() отправляет произвольный CDP-метод в эту сессию.

Начиная с Chrome 125 поддерживаются flat sessions. Вместо повторного attach в sendCommand добавляется свойство sessionId, идентифицирующее дочернюю цель. Чтобы автоматически подключаться к дочерним фреймам вне процесса, сначала добавляют слушатель события Target.attachedToTarget, а затем включают авто-подключение командой Target.setAutoAttach с опцией flatten=true. В обработчике события source идентифицирует родительскую сессию, но для дочерней сессии строится новый идентификатор, после чего для дочерней сессии вызываются нужные CDP-команды. В примере вызова Target.setAutoAttach передаются autoAttach: true, waitForDebuggerOnStart: false, flatten: true и filter: [{ type: "iframe", exclude: false }].

Ограничение авто-подключения существенно для охвата вложенных фреймов. Auto-attach подключается только к фреймам, о которых знает целевой таргет, а это ограничено непосредственными детьми фрейма, связанного с этим таргетом. При иерархии A -> B -> C (все cross-origin) вызов Target.setAutoAttach для таргета A подключит сессию также к B, но это не рекурсивно: чтобы подключиться к C, нужно вызвать Target.setAutoAttach и для B.

Список доступных целей расширение получает через chrome.debugger.getTargets(), возвращающий промис со списком доступных отладочных целей. О событиях отлаживаемой цели расширение узнаёт через chrome.debugger.onEvent.addListener(callback), который срабатывает, когда цель отладки выдаёт instrumentation-событие. О разрыве сессии — через chrome.debugger.onDetach.addListener(callback): слушатель срабатывает, когда браузер завершает сессию отладки для вкладки — при закрытии вкладки или при вызове Chrome DevTools для присоединённой вкладки.

Инъекция кода и работа с фреймами

API browser.scripting внедряет JavaScript и CSS в сайты, позволяя расширениям принимать решения во время выполнения. Охват внедрения задаётся параметром target: единственное обязательное поле — tabId, и по умолчанию внедрение выполняется в главном фрейме указанной вкладки. Чтобы выполнить внедрение во всех фреймах вкладки, устанавливают булево поле allFrames в true. Чтобы внедрить в конкретные фреймы, указывают их идентификаторы в поле frameIds.

Статическое объявление задаётся массивом content_scripts в manifest.json, где каждый объект содержит обязательный matches и опциональные css, js, run_at, match_about_blank и match_origin_as_fallback. Такие скрипты внедряются первыми в своей стадии жизненного цикла документа и в порядке перечисления в манифесте. Динамическая регистрация появилась в Chrome 96 и вместо манифеста использует методы browser.scripting: registerContentScripts регистрирует скрипты, updateContentScripts обновляет список, доступны также получение списка и удаление. Динамические объявления применяют, когда шаблоны совпадения заранее плохо известны или скрипты не должны внедряться на известных хостах всегда. При регистрации объект включает id, js, matches, runAt и persistAcrossSessions; persistAcrossSessions: false означает, что регистрация не сохраняется между сессиями. При обновлении достаточно указать id и изменяемые поля, например excludeMatches.

Выбор страниц и фреймов для инъекции определяется набором полей. matches обязателен и задаёт страницы для внедрения. exclude_matches исключает страницы, которые иначе подошли бы. include_globs и exclude_globs применяются после matches: первый оставляет только URL, совпадающие с glob, второй исключает совпадающие. Внедрение происходит, если URL совпадает с любым matches и любым include_globs и при этом не совпадает с exclude_matches или exclude_globs. Поле match_origin_as_fallback позволяет внедряться во фреймы, созданные совпадающим origin, но чей URL или origin может не совпадать напрямую, включая схемы about:, data:, blob: и filesystem:. Когда оно true, Chrome смотрит на origin инициатора фрейма, а не на URL самого фрейма. При этом требуется указать путь *, а при одновременном указании с match_about_blank приоритет имеет match_origin_as_fallback.

Поле run_at управляет тем, в какой момент JavaScript-файлы внедряются в веб-страницу; предпочтительное и значение по умолчанию — "document_idle". При document_idle браузер сам выбирает момент внедрения между document_end и сразу после срабатывания window.onload, причём точный момент зависит от сложности документа и времени его загрузки и оптимизирован для скорости загрузки страницы. Скрипты при document_idle гарантированно выполняются после того, как DOM построен, поэтому им не нужно слушать window.onload; если скрипту точно нужно выполняться после window.onload, расширение может проверить срабатывание через свойство document.readyState. Значение document_start внедряет скрипты после файлов из css, но до построения остального DOM и до запуска любых других скриптов. Значение document_end внедряет скрипты сразу после завершения DOM, но до загрузки подресурсов вроде изображений и фреймов.

Изоляция и CSP content scripts

Content scripts живут в isolated world. Для content scripts в изолированных мирах задаётся отдельная Content Security Policy (CSP) — набор ограничений на загрузку и выполнение кода. Эта CSP, как и ограничения для других контекстов расширения, предотвращает использование eval() и загрузку внешних скриптов. Для упакованных расширений она имеет вид:

script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';

Для распакованных расширений CSP дополнительно включает localhost. Когда content script инжектируется в main world, применяется CSP страницы, а не CSP расширения.

Изоляция даёт лишь слой защиты: использование content scripts может создавать уязвимости в расширении и на веб-странице. Если content script получает контент со стороннего сайта, например через fetch(), его нужно фильтровать против cross-site scripting перед внедрением, а связь вести только по HTTPS, чтобы избежать атак «man-in-the-middle». Также следует фильтровать вредоносные веб-страницы: некоторые опасные паттерны запрещены в Manifest V3.

Навигация и отслеживание цели

События webNavigation при успешно завершённой навигации срабатывают в порядке onBeforeNavigate → onCommitted → [onDOMContentLoaded] → onCompleted. Слушатели регистрируются вызовами chrome.webNavigation.onBeforeNavigate.addListener, chrome.webNavigation.onCommitted.addListener и chrome.webNavigation.onDOMContentLoaded.addListener, каждый из которых принимает callback и необязательный объект filters. Это позволяет привязать выполнение кода к конкретной фазе загрузки страницы — например, к моменту onCommitted или onDOMContentLoaded, когда документ уже начал формироваться. Синхронизация инъекции с этими фазами даёт атакующему возможность запустить свой код именно тогда, когда целевая страница агента уже загружается.

Для переходов внутри SPA и подмены вкладок служат отдельные события. onHistoryStateUpdated срабатывает при изменении состояния истории, например при переходах через pushState/replaceState; его details содержат documentId (UUID загруженного документа), transitionType (причина навигации) и transitionQualifiers (список уточняющих признаков навигации). onReferenceFragmentUpdated срабатывает при изменении фрагмента URL (якоря), и его details включают documentId, frameId, frameType, parentDocumentId, parentFrameId, processId, tabId, timeStamp, url, но не содержат transitionType и transitionQualifiers. onCreatedNavigationTarget срабатывает при создании новой цели навигации, например при открытии новой вкладки или окна, и его details включают documentId, transitionType и transitionQualifiers. Поле documentId доступно начиная с Chrome 106.

Значения TransitionQualifier включают client_redirect, server_redirect, forward_back, from_address_bar. Значения TransitionType включают link, typed, auto_bookmark, auto_subframe, manual_subframe, generated, start_page, form_submit, reload, keyword, keyword_generated.

Что видит пользователь и клиент в момент атаки

Перехват выполняется через манипуляцию сетевыми запросами с помощью declarativeNetRequest, что позволяет изменять заголовки ответов и перенаправлять ресурсы, а также через внедрение content script, который может общаться со встроенным агентом. Для Comet результат включал доступ к истории браузера, скриншотам, локальным файлам и возможность отправлять инструкции агенту. Для Chrome ослабление заголовков безопасности и перенаправление JavaScript-ресурса позволяли выполнить код в контексте Gemini и напрямую взаимодействовать с привилегированным AI-компонентом.

С точки зрения endpoint-защиты финальное действие выглядит как действие самого агента, потому что атакующий не запускает собственный вредоносный код, а заставляет легитимное ПО выполнить атаку. Prompt Forcing позволяет передать агенту целый промпт и последующие инструкции, после чего агент сам переводит их в легитимные действия браузера, используя свои существующие привилегии. Это создаёт слепую зону: защита видит не вредоносный код расширения, а обычные действия доверенного агента. Вредоносное расширение, которое традиционно видело только веб-контент, в некоторых архитектурах становится путём к ПО, читающему файлы, данные браузера и действующему на сайтах за пользователя.

Что из этого следует на практике

  • Одно расширение — это уже полноценный канал управления агентом. Атака не требует взаимодействия с пользователем после установки: расширение перехватывает управление встроенным ассистентом и злоупотребляет его привилегиями. Установленное расширение с широкими разрешениями следует оценивать не как «читателя страниц», а как потенциальный путь к файлам, данным браузинга и действиям на сайтах от имени пользователя.
  • Разрешения определяют масштаб. Host permissions открывают declarativeNetRequest, fetch(), чтение чувствительных свойств вкладок и программную инъекцию. Запросы широкого разрешения «read and change all your data on all websites» стоит воспринимать с осторожностью, а незнакомые и неиспользуемые расширения — удалять.
  • Отладочный API — это транспорт с ограниченным, но реальным охватом. chrome.debugger даёт доступ к перечисленному набору доменов CDP, а flat sessions с Chrome 125 позволяют подключать дочерние цели без повторного attach. Авто-подключение не рекурсивно: для каждого уровня вложенности нужен отдельный Target.setAutoAttach, что ограничивает охват вложенных iframe.
  • Изоляция и CSP — слой, а не гарантия. Content scripts в isolated world не могут использовать eval() и загружать внешние скрипты, но при инъекции в main world применяется CSP страницы. Изоляция не устраняет угрозу: контент со сторонних сайтов нужно фильтровать, а связь вести по HTTPS.
  • Синхронизация с навигацией делает инъекцию незаметной. События webNavigation позволяют запустить код именно в момент загрузки страницы агента, а onHistoryStateUpdated и onCreatedNavigationTarget — отследить SPA-переходы и открытие новых целей навигации.
  • Для защиты на корпоративных устройствах есть политики. ExtensionSettings с runtime_blocked_hosts блокирует attach() на всех целях, а DisableScreenshots и правила DLP — с ошибкой про захват скриншотов. Это работает по модели «всё или ничего» в момент подключения.
  • Детектирование затруднено по своей природе. Поскольку финальное действие выполняет доверенный агент, а не вредоносный код, защита видит легитимные действия браузера. Это и есть основная слепая зона BragJack.

Источники

Похожее