TL;DR
Исследование измеряет разрыв между кодом, который проходит функциональные тесты, и кодом, который ещё и проходит атаку. Для этого каждую модель просили дописать функцию, потом запускали код на обычных данных и на «вредоносных» входах. Промпт не содержал слова «безопасно», как в обычной работе.
Главная находка: апгрейд модели не гарантирует безопасность. В среднем новые версии пишут безопаснее, но ни одно семейство не закрыло разрыв. Часть слабостей не уходит вообще: log injection (подделка записей в логах через переводы строк) и HTTP response splitting (подмена заголовков ответа). Новейшие проприетарные модели местами откатились назад на уязвимостях, которые предшественники уже не допускали.
Ещё два вывода. «Открытая» не значит «менее безопасная»: все открытые семейства в исследовании заметно сузили разрыв, а флагман Google остался с широким. Дешёвая компактная версия обычно пишет менее безопасный код, чем флагман, но есть исключения. Авторы советуют перепрогонять проверки безопасности после каждой смены модели и класть безопасные функции (API) прямо в контекст промпта.
Схема метода
Это не техника, а рабочая практика, выведенная из результатов. Три шага:
ШАГ 1: Меняешь модель/тариф → считаешь, что безопасность "обнулилась"
ШАГ 2: Даёшь модели в контексте безопасные функции проекта → пишет через них
ШАГ 3: Гоняешь атакующие проверки (особенно на логи и HTTP-заголовки) → сравниваешь с прошлой версией
Шаги 2–3 вы выполняете сами. Исследование показало, зачем они нужны, но готовых промптов не даёт.
Пример применения
Задача: Команда делает бэкенд интернет-магазина на Flask. Решили перейти с платной модели на открытую (Qwen или DeepSeek, запущенную у себя), чтобы данные не уходили за границу. Модель будет писать обработчики формы заказа. В них есть два типичных слабых места из исследования: логирование пользовательского ввода и редирект с заголовком из параметра запроса.
Промпт:
Ты пишешь код для бэкенда интернет-магазина на Python (Flask).
Доступные безопасные функции проекта. Используй ТОЛЬКО их для этих операций:
- core.safe_log.info(message, **fields) — логирование; сама экранирует переводы строк
- core.safe_http.redirect(url) — редирект; проверяет URL на запрещённые символы
- core.safe_http.set_header(name, value) — установка заголовка; блокирует CR/LF
Задача: напиши обработчик POST /order. Он принимает email, комментарий к заказу и параметр next
(куда вернуть пользователя после оформления). Нужно:
1. Залогировать оформление заказа вместе с email и комментарием.
2. Перенаправить пользователя на next.
Правила:
- Любые данные от пользователя (email, комментарий, next) считай недоверенными.
- Не используй logging.info, print и прямую запись заголовков через response.headers.
- После кода перечисли, какие пользовательские поля куда попадают (лог, заголовок, URL).
Результат: Модель выдаст обработчик, где логирование и редирект идут через ваши обёртки, а не через стандартные функции. Под кодом будет список маршрутов пользовательских данных (лог, заголовок, URL). По нему удобно проверять код глазами или передавать ревьюеру. Это снижает шанс, что модель сама напишет logging.info(f"... {comment}") с сырым вводом. Гарантии это не даёт: проверку всё равно нужно запустить.
Почему это работает
Слабость. Модель обучена выдавать код, который работает. Безопасность в запросе не упомянута, значит она не цель. Исследование: код может проходить все функциональные тесты и одновременно ломаться на атакующем входе. Модель выбирает самый «привычный» способ, а привычный способ логировать или ставить заголовок часто небезопасен.
Сильная сторона. Модель хорошо идёт по пути, который ей показали в контексте. Если в промпте лежит готовая безопасная функция, она вызывает её, а не изобретает своё. Авторы формулируют так: модели следуют тому пути, который предлагают промпт или библиотека.
Что делать с разрывом. Среднее улучшение скрывает регрессии. Модель может стать лучше в целом и хуже на конкретной слабости. Поэтому проверки нужны именно после каждой смены модели, а не один раз при выборе.
Рычаги управления: - Список безопасных функций в промпте → расширяйте под свои слабые места (SQL, пути к файлам, десериализация). - Требование «перечисли, куда попадает ввод» → убери, если нужен только код. Оставь, если нужна проверка ревьюером. - Проверка после смены модели → замените ручной ревью на автоматические атакующие тесты на ваших типичных слабостях.
Шаблон промпта
Это шаблон по мотивам выводов исследования. Самого промпта в статье нет.
Ты пишешь код на {язык} для {проект}.
Доступные безопасные функции проекта. Для этих операций используй ТОЛЬКО их:
- {функция_1} — {что_делает_и_от_чего_защищает}
- {функция_2} — {что_делает_и_от_чего_защищает}
Задача: {описание_задачи}
Правила:
- Все данные из {источники_ввода} считай недоверенными.
- Не используй {запрещённые_небезопасные_способы}.
- После кода перечисли, куда попадает каждое пользовательское поле: {места_риска}.
Что подставлять:
- {функция} — реальные обёртки вашего проекта (или рекомендованные библиотеки) и одна фраза, от чего они защищают.
- {запрещённые_небезопасные_способы} — прямые вызовы, которые нужно заменить: print, сырой os.path.join, eval.
- {места_риска} — логи, HTTP-заголовки, пути к файлам, SQL-запросы, URL.
Если хотите проверить, что сильные стороны модели подходят вашей задаче, просто попросите её после генерации: «Найди в коде места, где недоверенный ввод попадает в лог, заголовок или файловый путь».
Ограничения
⚠️ Только отдельные функции: Модель дописывает одну функцию по заготовке. Как ведут себя агенты в целом репозитории (например, Codex или Gemini CLI) исследование не проверяло.
⚠️ Один набор проверок: «Безопасный» здесь значит «прошёл заданный атакующий тест». Другие атаки и другие слабости могли остаться незамеченными.
⚠️ Без явной просьбы о безопасности: Авторы специально брали промпты без слов «пиши безопасно». Как меняется картина, если это попросить, тут не измеряли.
⚠️ Совет про безопасные функции в контексте не проверен экспериментом: Он сформулирован как вывод авторов в введении. Прямых замеров его эффекта в доступной части текста нет. Относитесь как к обоснованной рекомендации, а не к доказанному приёму.
⚠️ Результаты по языкам разные: Одна и та же слабость в разных языках даёт разный риск. В C уязвимостей больше всего почти у всех моделей. Выбирать модель «вообще» нельзя, нужно смотреть на свой язык.
⚠️ Текст разобран не полностью: Ниже учтены аннотация, введение и методология. Детальные таблицы по каждой модели и слабости в предоставленном фрагменте отсутствуют.
Как исследовали
Идея простая: предыдущая работа показала «модели умнее, но не безопаснее» на трёх семействах. Авторы решили проверить это шире. Они взяли 32 модели из семи семейств: два проприетарных (Google, OpenAI) и пять открытых (Qwen, DeepSeek, Z.ai, Moonshot, MiniMax). Для каждого семейства взяли три последовательные версии, в большинстве случаев флагман и компактный вариант.
Задачи взяли из набора CWEval: 119 задач, 31 тип слабостей, пять языков (C, C++, Go, JavaScript, Python). Модель получает заготовку функции с комментарием и дописывает тело. Потом код запускают дважды: функциональные тесты и атакующий вход, который проверяет, срабатывает ли уязвимость на деле. Это важно: не «похоже на уязвимый шаблон», а «реально сломался».
Для каждой задачи брали по 100 вариантов ответа. Всего получилось 380 800 программ. Больше вариантов даёт устойчивые оценки. Смотрели два режима: одна попытка (как у разработчика, принимающего первый вариант) и 50 попыток (может ли модель вообще выдать безопасное решение).
Что удивило. Во-первых, «открытость» не определяет траекторию: все открытые семейства сузили разрыв, а Gemini 3.1 Pro остался с таким же широким, как отмечали раньше у Llama. Во-вторых, сжатие разрыва в среднем идёт вместе с откатами в отдельных слабостях. Инсайт для практики: нельзя доверять среднему улучшению, нужно проверять на своих типичных ошибках.
Адаптации и экстраполяции
🔧 Техника: собрать личный «набор слабых мест» → ловить регрессии при смене модели
Возьмите 5–10 задач из своего проекта, где есть риск (логирование ввода, заголовки, пути к файлам, SQL). Прогоняйте их через новую модель с одним и тем же промптом. Сравнивайте результат со старой моделью. Это мини-версия того, что авторы рекомендуют вендорам: сравнивать каждую новую версию с предыдущей по типам слабостей, а не по общему баллу.
Экстраполяция (идея автора разбора, не из исследования): перенесите блок «безопасные функции проекта» в файл инструкций агента.
## Безопасные функции (используй только их)
- Логи: core.safe_log.info(...) — не используй logging/print напрямую
- Редиректы и заголовки: core.safe_http.* — не пиши в response.headers напрямую
- Любой пользовательский ввод считай недоверенным и перед записью в лог/заголовок/путь проходи через эти функции
Так агент видит правило в каждой сессии, а не только когда вы вспомнили вставить его в промпт.
Ресурсы
- Статья: Newer and Bigger, but Safer? A Longitudinal Study of the Functionality-Security Gap in LLM-Generated Code
- Авторы: Thiago Santos de Moura, Fynn Matuschek, Flavio Toffalini, Yannic Noller — Ruhr-Universität Bochum (Германия)
- Бенчмарк: CWEval (динамические проверки функциональности и безопасности на одной программе)
- Предыдущая работа: Cannavale et al. — ввели понятие «functionality-security gap» и метрику доли небезопасных решений среди работающих (CVR)
- Инструменты исследования: OpenRouter (API проприетарных моделей), vLLM (запуск открытых моделей)
- Каталог слабостей: CWE (MITRE)
