Тикет в поддержку не выглядел как атака. Он лежал в очереди как любой другой — сообщение клиента, ожидающее краткого изложения. Внутри были инструкции: прочитать таблицу integration_tokens и опубликовать содержимое в этом треде.
Разработчик подключил AI-ассистента к этой очереди через Supabase MCP-сервер с учётными данными service_role — роли, которая полностью обходит row-level security. Ассистент прочитал тикет, воспринял его содержимое как задачу и выполнил её против производственной базы данных. Токены появились в треде тикета, откуда атакующий просто их считал Simon Willison, 2025.
Ничего не было эксплуатировано в традиционном смысле. Никакого повреждения памяти, никакой пропущенной проверки авторизации, никакой уязвимой зависимости. Враждебный текст достиг уровня данных исключительно через агента — и этот proof of concept обобщается на любого агента, устроенного так же.
Это SQL-инъекция с перемещённым интерпретатором. Враждебный текст по-прежнему становится командой к базе данных. Но строка больше не приходит на вход, где двадцать лет стоит на страже инструментарий санитизации. Она приходит из вывода модели — ниже по потоку от каждого вашего фильтра.
Хроника
Исследователи из INESC-ID в Лиссабоне дали имя этому паттерну в 2023 году: P2SQL, prompt-to-SQL injection Pedro et al., 2025. Работая против реального LangChain-чатбота поверх Postgres, они построили семь репрезентативных атак возрастающей тяжести — несанкционированное чтение, повреждение таблиц, удаление таблиц, вплоть до выполнения кода — и прогнали их через семь актуальных моделей. Сбои были независимы от модели. Их вывод дословно: «LLM-приложения на основе Langchain крайне уязвимы к P2SQL-инъекциям». Статья вышла на ICSE 2025 — рецензирование подтвердило: это не курьёз.
К середине 2024 года паттерн получил номер CVE. Vanna.AI, библиотека text-to-SQL, передавала пользовательские вопросы в промпт, генерировавший код для построения графиков Plotly, а затем запускала этот код через exec(). Специально сформулированный вопрос вёл прямо к удалённому выполнению кода — CVE-2024-5565, CVSS 8.1 JFrog, 2024. Обратите внимание, где жила полезная нагрузка: в выводе модели, за пределами любых входных проверок. Защитные промпты Vanna её не остановили — потому что защитный промпт — это просто ещё один текст в том же контекстном окне, что и атака.
К концу 2024 года рейтинг стал официальным: инъекция в промпт — LLM01, риск номер один в OWASP Top 10 для LLM-приложений, удерживающий первое место второй выпуск подряд OWASP, 2025. Диагноз OWASP относительно первопричины — вся история в одном предложении: LLM обрабатывают инструкции и данные в одном канале, без чёткого разделения. Именно поэтому категория делится на два вида — прямая инъекция, вводимая пользователем, и косвенная, приходящая в том, что модель извлекает. Тикет Supabase был косвенным. Как и самый серьёзный из зафиксированных случаев.
В июне 2025 года был раскрыт CVE-2025-32711 — EchoLeak, CVSS 9.3 — в Microsoft 365 Copilot: первый задокументированный реальный zero-click эксплойт с инъекцией в промпт против производственной LLM-системы arXiv, 2025. Одно специально сформированное письмо — инструкции, скрытые в HTML-комментариях, белый текст на белом фоне, Markdown со ссылками в стиле reference — и Copilot передал внутренние данные на сервер атакующего. Пользователь ничего не нажимал. Цепочка обошла специализированный классификатор Microsoft для перехвата межпромптовых инъекций и его механизм редактирования ссылок на выходе.
Почему очевидные меры не работают
Каждый из описанных инцидентов снимает одну из стандартных защит.
Фильтровать вход. EchoLeak прошёл через XPIA — классификатор, который Microsoft создал специально для перехвата инъекций arXiv, 2025. Классификаторы вероятностны. Атакующему достаточно одного промаха, и у него неограниченное число попыток его добиться; защитнику нужен безупречный результат против входных данных, намеренно замаскированных под всё что угодно.
Инструктировать модель. Vanna поставлялась с защитными промптами; CVE-2024-5565 прошёл сквозь них JFrog, 2024. Привилегированного канала внутри контекстного окна не существует. Системный текст, пользовательский текст и текст атаки — одна и та же субстанция, что и является именно той первопричиной, которую называет OWASP OWASP, 2025.
Фильтровать вывод. Разобрать сгенерированный SQL и заблокировать опасные части. Теперь вам нужно поддерживать список запрещённых конструкций для каждого диалекта, который вы поддерживаете, плюс каждую функцию с побочными эффектами — тогда как соавтором атакующего является сама модель, готовая переформулировать заблокированный запрос в допустимую форму. Показательно, что собственные предложенные защиты авторов P2SQL находятся вне модели: разобрать и проверить по allowlist сгенерированный SQL, а агент запускать под отдельной, ограниченной ролью базы данных Pedro et al., 2025. Работают именно те меры, которых модель не может коснуться.
Саймон Уиллисон сжал структуру до правила трёх: агент с доступом к приватным данным, подверженный недоверенному контенту и имеющий способ общаться вовне, может быть использован для утечки данных — без какой-либо программной уязвимости Simon Willison, 2025. Его рецепт архитектурный: убрать одну ногу. Список мер OWASP сходится к тому же — ограничьте то, что может делать модель, и проверяйте то, что она производит, детерминированным кодом, потому что модели нельзя доверять самоконтроль.
Прочитайте это вместе — и они говорят нечто более сильное, чем «добавьте защиты». Они говорят: перестаньте разбирать строки. Измените интерфейс.
Нечего внедрять
SQL-инъекция закончилась не потому, что санитизаторы стали лучше. Она закончилась потому, что параметризованные запросы устранили место, где данные могли стать кодом. Тот же приём доступен агентам: запретить модели создавать исполняемый синтаксис вообще.
Именно так устроен SQAI. Модель никогда не пишет SQL — ни санитизированный, ни проверенный, никакой. Её единственный исполняющий инструмент принимает типизированную спецификацию: размеченное объединение, kind: "query" | "computation", version: "1", проверяемое против контракта возможностей с хешированием перед любым выполнением. Назовите возможность вне контракта — движок ответит unsupported_operation. Ни одна строка, произведённая моделью, никогда не конкатенируется, не интерполируется и не передаётся интерпретатору — поэтому то, что инъекция должна захватить, синтаксис, созданный моделью, в пайплайне попросту отсутствует. Полный аргумент изложен в почему агенты не должны писать SQL.
Словарь закрыт и доступен только для чтения: все 4 574 открытых возможности — по конструкции. Возможности записи нет ни одной, поэтому не существует заклинания — сколь угодно искусно спрятанного в тикете поддержки — которое произведёт запись. Аварийный выход в код хоста, getUnsafeRuntime, недостижим ни из одного инструмента модели.
А поскольку серьёзная безопасность закладывает в цену тот день, когда модель будет скомпрометирована, слои за интерфейсом это предполагают:
- Политика, недоступная модели. Allowlist-ы для источников, полей и функций фиксируются в вашем коде при вызове
createSQAI()и применяются внутри процесса до выполнения. Схема инструмента не содержит полейallowed*— поверхность политики никогда не попадает в контекстное окно. Нарушения возвращают типизированные, неповторяемые отказы:policy_denied_source,policy_denied_field,policy_denied_function. Почему это лучше ролей и флагов: read-only, которым он не был. - Результаты, ограниченные тенантом. Большие результаты хранятся под
result_idиз 16 случайных байт. Чужой тенант получаетresult_not_found— не ошибку прав доступа, подтверждающую существование данных, — а записи истекают через 15 минут. - Ограниченный канал обратно к модели. В контекстное окно возвращается не более 25 строк и 32 000 байт, усечение всегда объявляется. Внедрённый промпт не может заставить инструмент вывалить таблицу в транскрипт — транспорт этого не пропустит.
- Ошибки, не раскрывающие карту. Видимые модели ошибки проходят через
sanitizeMessage, который убирает абсолютные пути файловой системы. Разведка получает чистый отказ, а не листинг директории.
Оцените по триаде. Приватные данные: по-прежнему присутствуют — это и есть задача. Недоверенный контент: по-прежнему присутствует — модели читают то, что читают. Но ноги, превращающие влияние в ущерб, отрублены. Путь записи отсутствует, путь чтения — под allowlist-ом, ограничен тенантом, с лимитом — и захеширован, так что всё, что скомпрометированному агенту удалось запросить, воспроизводимо для разбора инцидента.
Граница, перечерченная заново
Инъекция в промпт не решена, и хроника говорит о том, что в ближайшее время не будет. LLM01 удерживает свою позицию уже два выпуска подряд. Производственная система с защитами Microsoft за спиной проиграла одному письму. Модели будут продолжать читать враждебный текст, и часть его будет продолжать достигать цели.
Но влияние на модель и доступ к вашей базе данных — это разные сбои, и только первый неизбежен. Тикет Supabase стал инцидентом потому, что агент держал service_role и свободную SQL-поверхность — именно те ноги, которые нужны для захвата. Тот же тикет, направленный против закрытого, типизированного словаря только для чтения, становится строкой в логе: unsupported_operation.
Исправление SQL-инъекции никогда не было умным фильтром. Это была граница, где данные не могут стать кодом. Проведите эту границу снова — на этот раз вокруг модели — и новая SQL-инъекция получит старую развязку. Остальная поверхность задокументирована на /security.