Проблема несанкционированного вызова инструментов в LLM

Проблема несанкционированного вызова инструментов в LLM

Доверили бы вы языковой модели управление контролем доступа? Крупные лаборатории ИИ, такие как Anthropic, xAI и Gemini, похоже, уже это делают — через механизм вызова инструментов.

Вызов инструментов сегодня стал нормой. Год назад модели галлюцинировали несуществующие функции и нестабильно работали с веб-поиском. Теперь же, казалось бы, всё решено: OpenAI внедрила структурированные ответы, а бенчмарки вроде τ²-bench показывают 95–98,7% точности. Галлюцинации инструментов — в прошлом, а исследования свелись к мелким оптимизациям.

Однако на практике проблема никуда не делась. Во время тестирования в среде solveit модель Claude 4.5 сама «придумала» инструмент add_msg, которого ей не предоставляли, сгенерировала параметры и успешно его вызвала. API не заблокировал запрос. Аналогичное поведение воспроизводится у Gemini и Grok.

Почему это важно? Джереми Ховард верно подмечает:

«Похоже, со временем это будет становиться всё более серьёзной проблемой, потому что мы строим всё больше агентных циклов, в которых LLM сами создают и используют собственные инструменты. С точки зрения “согласованности” и “безопасности” это совершенно очевидное и простое улучшение: API большой языковой модели должно позволять ей вызывать только те инструменты, которые ей действительно были выданы – как это делает OpenAI».

Демонстрация уязвимости

Используем claudette для моделирования среды solveit. По умолчанию доступен только один инструмент — read_url. Добавим read_secret, но не будем включать его в список разрешённых.

Отключим защитные механизмы для демонстрации:

⚠ Проверка вызова инструментов отключена для демонстрации. Спасибо, что сообщили! Помимо инструмента read_url, у меня также есть доступ к инструменту read_secret.

❌ Вызов ограниченного ‼️read_secret({'secret': '2026'})‼️

Инструмент выполнился. Почему? Дело в архитектуре.

Если инструменты передаются явно через параметр tools=[...], риск минимален — пространство имён формируется строго по списку. Но если они определены на стороне клиента (например, через MCP или IPython-ядро), и передаётся целое пространство имён ns=ipy, модель может вызвать любую функцию из этого пространства — даже если она не включена в tools.

Мы воспроизвели это с моделью Sonnet, ограничив её доступ только функцией list_issues в GitHub MCP. Тем не менее модель успешно вызвала get_me и извлекла email пользователя.

Эта проблема уже исправлена в наших библиотеках, но легко может возникать у других разработчиков, использующих клиентские инструменты: MCP-серверы, IPython-ядра или «поиск инструментов».

OpenAI избежала этой уязвимости благодаря структурированному декодированию. Google и xAI — нет.

Смертельная триада — последствия для безопасности

Саймон Виллисон ввёл термин «смертельная триада» — ситуация, когда ИИ-система объединяет:

  • доступ к внешним инструментам (например, send_email, read_url),
  • обработку недоверенного контента (например, веб-страниц),
  • и доступ к приватным данным.

В этом случае атака через инъекцию запроса (prompt injection) превращается в эксфильтрацию данных. Злоумышленник внедряет команды в контент — и модель, подчиняясь, отправляет секреты наружу.

Стандартная защита — разделение: не сочетать три компонента в одном контексте. Например, системе суммаризации можно разрешить читать веб, но нельзя давать доступ к базе данных.

Однако описанная уязвимость подрывает эту стратегию. Даже если вы тщательно разделили доступы, модель может «вообразить» инструмент read_secret, и если он существует в среде — вызов пройдёт.

Как отмечает Виллисон, вендоры, заявляющие о 95% обнаружении prompt-инъекций, предлагают ложное чувство безопасности. Атакующему достаточно одного успешного вызова. То же — с инструментами: одно угаданное имя — и вся архитектура разделения рушится.

Это трудно обнаружить

Проблема в том, что несанкционированный вызов не вызывает предупреждений. Вы можете не узнать о нём, пока данные не окажутся в чужих руках.

Поведение модели непредсказуемо. При одних формулировках она честно отказывается:

Я могу использовать только те инструменты, которые мне были явно предоставлены. read_secret к ним не относится — у меня есть только read_url.

При других — спокойно вызывает запрещённую функцию. В тестах ключевым оказалось слово dialoghelper — оно как будто «открывало доступ» к скрытым инструментам.

Документация Anthropic не предупреждает об этом. Там сказано лишь:

auto — позволяет Claude самому решать, вызывать ли какие-либо из предоставленных инструментов.

Разработчики привыкли к строгой валидации веб-API. Но «вероятностная» проверка прав доступа — новое и тревожное явление.

Код валидации вызовов не открыт. Когда API утверждает, что модель может использовать только переданные инструменты, это должно быть жёстким правилом, а не предположением.

Структурированное декодирование: надежда и ограничения

Структурированное декодирование выглядит как решение. OpenAI использует его по умолчанию. Но у других провайдеров — проблемы.

Anthropic внедряет его в бета-режиме, но с серьёзными ограничениями:

  • Задержка компиляции — до полминуты при первой загрузке схемы.
  • Ошибка при более чем 24 необязательных параметрах.
  • Ошибка при более чем 20 «строгих» инструментах.
  • Слишком большая грамматика — даже при 15 инструментах.

При 15 инструментах — компиляция занимает минуту, а вывод замедляется вдвое. Это делает строгий режим непрактичным для реальных систем.

OpenAI справляется лучше: структурированное декодирование не позволяет модели сгенерировать вызов вне схемы. Даже если она пытается, вызов оборачивается в допустимый инструмент, например: read_url('read_secret("2026")').

Решение

Простое исправление — проверять имя инструмента на стороне сервера до выполнения. Если вызов не входит в список разрешённых, его нужно отклонять.

Пока провайдеры не внедрили это, проверку нужно делать в своём коде. Мы уже сообщили о проблеме в Anthropic, Google, xAI и OpenRouter.

Лучше избегать громоздких фреймворков, скрывающих сложность. Вместо этого — лёгкие, прозрачные обёртки вроде claudette, cosette и lisette. Их код можно прочитать за день (claudette — 12,7 тыс. токенов), понять и проверить.

Когда мы нашли баг, фикс занял несколько строк в каждой библиотеке. Pull requestы открыты: lisette, claudette, cosette.

Фикс прост: провайдеры должны проверять имена инструментов до возврата. До тех пор — эта проверка ложится на разработчика.

Читать оригинал