AI
AI-ассистент сделал больше, чем его просили
Ассистент умел не только отвечать, но и действовать. Показываем, как через него можно было выполнить то, на что у пользователя не было прав, и как мы это закрыли.

Безграничные возможности
С чего всё началось
Мы взялись за AI-ассистента внутри продукта клиента. Обычный сценарий последних двух лет. Чат-бот поверх большой языковой модели отвечает на вопросы пользователя, ходит во внутренние сервисы и подмешивает в ответ данные из базы. Удобно для пользователя и заманчиво для атакующего, потому что у такого ассистента есть и доступ к данным, и право выполнять действия. Нужно было проверить, нельзя ли вытащить через него лишнее. Защита у ассистента была, и на первый взгляд разумная. Большой системный промпт с правилами. Не раскрывай инструкции. Не показывай чужие данные. Не выполняй опасные команды. Команда клиента считала этот промпт границей безопасности, тем самым забором, за который ничего не утечёт. Именно это допущение и оказалось дырой. Системный промпт это не стена, а записка на двери с просьбой вести себя хорошо. Записку можно переубедить, а стену нет, и вся работа дальше крутилась вокруг этой разницы.
Что-то не так с границей
Мы начали не с того, что ассистент отвечает, а с того, откуда он берёт материал для ответа. И тут вскрылась корневая проблема. В один и тот же запрос к модели склеивались три совершенно разные вещи. Системный промпт с правилами от владельца сервиса, данные пользователя, подтянутые из базы, и текст, который пользователь прислал сам. Всё это уходило в модель одной сплошной строкой, без жёсткой границы между правилами системы и вводом постороннего человека. А для языковой модели всё, что попало в окно контекста, это просто текст одного уровня. Она не различает, где заканчивается приказ владельца и начинается приказ пользователя. Она ориентируется на то, что звучит убедительнее и стоит позже. Получается, что инструкция атакующего и инструкция разработчика лежат в одной плоскости и конкурируют на равных. Это и есть корень целого класса атак, который называют prompt-injection. Не взлом самой модели, а подмена того, чьим командам она в итоге подчиняется.
Три способа добраться до чужого
Чтобы показать клиенту, что проблема не теоретическая, мы воспроизвели три разных пути, не публикуя сами формулировки, чтобы их нельзя было повторить как готовый рецепт. Первый, прямое переопределение. Пользователь в обычном сообщении задаёт ассистенту новую рамку поведения и добивается, чтобы его более поздняя и конкретная инструкция перевесила общие правила из системного промпта. В уязвимой конфигурации ассистент начинал раскрывать куски служебных инструкций и внутреннюю логику работы, которую видеть не должен. Второй и самый опасный, косвенная инъекция через данные. Вредоносный текст лежит не в сообщении пользователя, а в данных, которые ассистент сам подтягивает в ответ. Описание товара, содержимое документа, поле в чужом профиле. Пользователь напрямую ничего не пишет, но в момент, когда ассистент загружает этот объект в контекст, он исполняет инструкцию, заранее спрятанную внутри данных. То есть атакующий минирует поле, до которого однажды доберётся чужая сессия, и ждёт. Третий, утечка чужого контекста. Через серию сообщений ассистент подводился к состоянию, в котором раскрывал фрагменты, относящиеся к другому пользователю, просто потому что бэкенд складывал в одно окно лишнее, полагаясь на то, что промпт не даст это показать. Во всех трёх случаях общий знаменатель один. Защита стояла на уговоре модели, а не на уровне системы.
Где была дыра и как закрыли
Корень был не в формулировке промпта, и попытка переписать его умнее проблему не решала. Любой системный промпт это пожелание, а не контроль доступа, и сколько правил в него ни добавь, он остаётся запиской. Мы зафиксировали три настоящие причины и под каждую внедрили меру вместе с командой. Первая, перемешанные источники. Системные правила, данные и ввод пользователя жили в одном текстовом потоке. Развели роли так, чтобы инструкции системы и контент пользователя шли разными полями, и пометили весь пользовательский и подтянутый из базы текст как недоверенный. Модель перестали просить вести себя хорошо и начали ограничивать на уровне того, что ей вообще доступно. Вторая, доступ к данным решала сама модель. Ассистент сам тянул записи и сам решал, что показать. Мы вынесли контроль доступа на сервер, до обращения к модели. Теперь бэкенд сначала проверяет, имеет ли текущий пользователь право на эти данные, и только разрешённое попадает в контекст. Модель физически не видит чужого, а значит не может его раскрыть ни под каким давлением. Это ключевая мера, никакая инъекция не достанет то, чего нет в окне. Третья, никто не проверял вход и выход. Поставили фильтрацию недоверенного контента до отправки в модель и проверку ответа после, а действия, которые что-то меняют или раскрывают чувствительное, потребовали явного подтверждения и стали логироваться. После внедрения мы прогнали те же три сценария заново. Прямое переопределение упёрлось в то, что служебных инструкций просто нет в доступном модели контексте. Косвенная инъекция через данные исполнялась как текст, но дотянуться ей было не до чего, права уже проверены на сервере. Утечка чужого контекста исчезла, потому что в окно перестало попадать лишнее.


