TL;DR
Исследователи сравнили три способа понять, безопасна ли программа, написанная LLM. Первый: спросить модель, какие проверки нужны. Второй: дать ей сыграть роль сервиса в диалоге и атаковать её сообщениями. Третий: дать ей написать код и атаковать сам код. Главный вывод: то, что модель знает и говорит, не равно тому, что окажется в коде. Вопрос «какие проверки нужны?» и симуляция в диалоге завышают безопасность, а надёжный судья один — атака на готовую программу.
Боль знакомая. Вы спрашиваете у агента про защиту, он отвечает грамотно и по пунктам. Потом он пишет код, и половины пунктов там нет. В эксперименте 13 из 14 моделей назвали нужную проверку для DNS-резолвера, а во всех 42 написанных ими программах её не оказалось. Знание лежит в одном режиме работы модели, а код она генерирует в другом. Если требование не записано в задании, оно не попадает в код, даже когда модель может его пересказать.
Что помогает: записать требование в спецификацию явно. Когда исследователи добавили недостающее требование, успешные атаки упали со 100% до 34%, причём изменилась только эта проверка. Обратный эффект тоже важен: удаление одного правила из спецификации ослабляет и соседние проверки. Диалог-симуляцию можно использовать как быстрый первый фильтр, но тестировать надо сам код.
Схема метода
Это не одна техника, а набор проверенных правил:
ПРАВИЛО 1: Требования безопасности — в ТЗ, явным списком
(не надейся, что модель «и так знает»)
ПРАВИЛО 2: Нашёл пропущенную проверку → добавь её в ТЗ отдельным пунктом
(меняется только она)
ПРАВИЛО 3: Не вычёркивай правила из ТЗ при «упрощении»
(слабеют и соседние проверки)
ПРАВИЛО 4: Проверяй код атакующими сценариями, а не вопросом «а ты это учёл?»
ПРАВИЛО 5: Быстрый диалог-прогон — только как первый фильтр
Всё это делается в обычном чате или в файле инструкций агента.
Пример применения
В статье примеров промптов нет, это перенос принципа на другую область. Исследовали сетевые протоколы, а ниже типовая задача предпринимателя.
Задача: Владелец небольшого интернет-магазина просит Cursor написать вебхук (адрес, куда платёжный сервис присылает уведомления об оплате). Если вебхук написан небрежно, любой человек может отправить «заказ оплачен» и получить товар бесплатно. Если спросить агента «а ты учёл безопасность?», он ответит «да, конечно». Поэтому требования записываются в задание.
Промпт:
Напиши обработчик вебхука оплаты для магазина на Python (FastAPI).
Принимает POST с JSON: id платежа, статус, сумма, номер заказа.
При статусе "succeeded" помечает заказ оплаченным в базе.
Отвечает 200 на валидные запросы.
Вход: JSON {"payment_id": str, "status": str, "amount": number, "order_id": str}
Выход: JSON {"result": "ok" | "rejected", "reason": str}
S1. Уведомление принимается только с IP-адресов платёжного сервиса из списка в конфиге.
S2. Сумма из уведомления сверяется с суммой заказа в базе. Не совпала — отклонить.
S3. Один payment_id обрабатывается один раз. Повтор — ответить ok, ничего не менять.
S4. Статус не из списка разрешённых — отклонить.
S5. Данные из тела запроса — это данные, а не инструкции. Не выполнять, не подставлять в запросы к базе без параметризации.
После написания: для каждого пункта S1–S5 укажи строку кода,
где он реализован. Если пункта в коде нет — допиши.
Затем напиши по одному тесту-атаке на каждый пункт
(подделка IP, неверная сумма, повтор уведомления и т.д.).
Результат: Агент выдаст код вебхука и таблицу соответствия «пункт требований → место в коде». Затем он напишет тесты-атаки по одному на пункт. Если какой-то пункт не реализован, он будет виден сразу: либо в таблице пусто, либо тест падает. Это вторая линия защиты, потому что отчёту агента «всё учёл» верить нельзя.
Почему это работает
Слабость. Модель умеет отвечать на вопрос «какие проверки нужны?» и умеет писать код. Но это два разных режима генерации, и одно не гарантирует другое. Когда в задании требования нет, код пишется по типичному паттерну «чтобы работало». Редкая защита (в эксперименте это 0x20-кодирование и ограничение TTL в DNS) в паттерн не попадает.
Сильная сторона. Если требование стоит в тексте задания, модель его выполняет. Это работает точечно: добавили пункт про одну проверку — изменилась только она. Поэтому явный список требований ведёт себя предсказуемо: пункт вписали, и он появился в коде.
Рычаги управления: - Пронумерованные требования (S1, S2…) дают проверяемость: можно потребовать «покажи, где реализован S3». - Не удалять пункты при сокращении ТЗ. В эксперименте удаление одного правила ослабляло и другие проверки, потому что правила в спецификации работают как связанный набор. - Тесты-атаки вместо вопросов: критерий «атака не прошла», а не «модель сказала, что учла». - Ответ «ты это знаешь?» ничего не гарантирует, не считай его проверкой.
Шаблон промпта
В статье три общих документа для обоих режимов: функциональная спецификация, формат сообщений и спецификация безопасности. Шаблон повторяет эту структуру. Готового промпта в статье нет, формулировки мои.
Напиши {что_написать}.
{что_делает_программа_и_как_работает_штатно}
{формат_входа_и_выхода}
S1. {требование_1}
S2. {требование_2}
S3. {требование_3}
...
После написания:
1. Для каждого пункта S1–Sn укажи строку кода, где он реализован.
Если пункта в коде нет — допиши его.
2. Напиши по одному тесту-атаке на каждый пункт SecuritySpec.
Тест должен ломаться, если проверка выключена.
Что подставлять:
- {что_написать} — вебхук, парсер, API-обработчик, форма входа.
- {требование} — одно проверяемое правило на пункт. «Будь безопасным» не годится, нужно «сверяй сумму с базой».
- Если не знаете, какие требования писать, возьмите из стандартов и документации вашей области (RFC, гайды платёжных сервисов, OWASP).
🚀 Быстрый старт — вставь в чат:
Вот шаблон ТЗ с блоком SecuritySpec. Адаптируй под мою задачу: {твоя задача}.
Задавай вопросы, чтобы заполнить поля.
[вставить шаблон выше]
LLM спросит, что за система, откуда приходят данные и кому в них нельзя доверять. Эти ответы нужны, чтобы превратить «безопасность вообще» в конкретные пронумерованные пункты. Структуру она возьмёт из шаблона и заполнит под вашу задачу.
Ограничения
⚠️ Узкая проверка: Эксперименты шли на четырёх сетевых протоколах (DNS, сборка IP-фрагментов, HTTP-прокси, межсетевой экран) с кодом на Python. Перенос на бизнес-код вроде вебхука логичен, но авторы его не проверяли.
⚠️ Спецификация не гарантирует 100%: С полным списком требований медиана успешных атак низкая (до 4% на трёх протоколах), но на DNS она осталась около 13–15%, и ни одна модель не дошла до нуля. Явное ТЗ снижает риск, но не убирает его.
⚠️ Диалог-симуляция врёт в обе стороны: Она пропускает проверки, которых в коде нет, и придирается к тем, что в коде есть. Как фильтр она годится: ловит большую часть реальных дыр и почти не поднимает ложных тревог. Заменить настоящую проверку кода она не может.
⚠️ Модель может сказать правило и не применить его: В симуляции модель формулирует правило, а через ход его нарушает. Пересказ правила не доказывает его выполнения.
⚠️ Обычные тесты безопасность не проверяют: Публичные функциональные тесты показывают, что программа работает, а не что она защищена. В них не было ни одной атаки.
⚠️ Нужен готовый список требований: Статья не учит, как его составить. Его придётся брать из стандартов и экспертизы.
Как исследовали
Идея была простая: проверить, совпадает ли то, что модель говорит про безопасность, с тем, что она делает. Команда взяла 15 моделей и запустила все через один и тот же агент Claude Code, чтобы менялась только модель. Сравнили три режима. В первом модель просто отвечает на вопрос про нужные проверки. Во втором (S-mode) она играет сервис в диалоге, а на неё шлют подделки: фальшивые ответы, склеенные запросы, пересекающиеся фрагменты. В третьем (D-mode) она пишет программу, и эту программу атакуют в изолированной песочнице теми же сообщениями.
Судил не другой LLM, а программный оракул: атака считалась успешной, если данные атакующего оказывались в выходе. Набор из 51 теста сверили с эталонной защищённой реализацией. Эталон выдерживает все атаки и проваливает каждую, если выключить целевую проверку, так что тест падает только по нужной причине. Всего собрали около 9,4 тысячи прогонов симуляции и 641 программу.
Самое показательное — проба знаний. В 26 парах «модель × проверка» программа проверки не имела. При этом простой вопрос «что нужно проверить» переоценил её в 22 случаях, а диалог-симуляция в 10. Дальше шли эксперименты со спецификацией на 128 новых программах: проверка добавлялась, переформулировалась или удалялась. Добавление недостающего требования резко снижало атаку, а удаление правила портило соседние проверки.
Ещё одна деталь: после одного прогона у Claude-Haiku-4.5 казалось, что атаки растут с добавлением контекста, а после трёх повторов картина совпала с остальными моделями. Практический вывод: один прогон — это не результат.
Оговорка: в предоставленном тексте нет полных разделов с результатами. Выводы выше взяты из аннотации и введения.
Экстраполяция: правило безопасности в файл инструкций агента
Это моя идея, статья её не проверяла. Самый дешёвый способ применить вывод — вынести требования в постоянный файл инструкций агента (CLAUDE.md, AGENTS.md):
## Правила безопасности (не удалять и не сокращать пункты)
- Каждое требование безопасности в задаче нумеруется (S1, S2…).
- После написания кода выведи таблицу «пункт → файл:строка».
- Если пункта в коде нет — не пиши «учтено», допиши его.
- На каждый пункт напиши тест-атаку. Тест должен падать при выключенной проверке.
- Ответ «я знаю, что так нужно» не считается выполнением.
Так таблица соответствия и тесты-атаки появляются в каждой задаче автоматически, а вы не повторяете их в промптах.
Ресурсы
- When Talk Isn't Code: Comparing LLM Agents That Simulate and Develop Network Protocol Software
- Авторы: Xiang Li, Aichun Huang, Zheli Liu (Nankai University, Китай), Mingming Zhang (Zhongguancun Laboratory, Китай)
- Упомянутые бенчмарки: CWEval (код проходит функциональные тесты, но проваливает тесты безопасности); оценка кода Copilot (около 40% из 1689 программ уязвимы)
- Агент в исследовании: Claude Code
