TL;DR
P2Patch — схема из двух ролей, где исправление само становится мишенью. Агент-«атакующий» пишет запускаемый пример атаки (PoV, proof of vulnerability). Агент-«исправляющий» чинит код, пока этот пример не перестанет срабатывать. Потом атакующий получает уже исправленный код и ищет новый, отличающийся обход той же уязвимости. Каждый найденный обход навсегда остаётся в списке требований, и финальный патч должен блокировать все накопленные атаки. Успех фиксирует не сам агент, а реальный запуск, а оценка идёт отдельными проверками, которых агенты не видели.
Главная находка: исправление, которое блокирует показанную атаку, ещё не исправляет уязвимость. Агент чинит ровно то, что ему показали. Он закрывает один путь, одну кодировку или один вход, а соседние остаются открытыми. Это выглядит как успех: исходный пример атаки больше не срабатывает. Потом проверка идёт теми же данными, на которых писали фикс, и ловить нечего. В исследовании после того, как патч заблокировал исходную атаку, атакующий всё равно находил подтверждённый запуском обход в 41% прогонов.
Метод добавляет к обычному «починил → проверил» третий шаг: «починил → попытался сломать именно этот фикс → починил с учётом нового обхода». Цикл ограничен по числу раундов. Исходный PoV и все найденные обходы перепроверяются на финальном патче. Три исхода запуска (атака сработала, заблокирована, неопределённо из-за сбоя сборки или таймаута) не смешиваются, иначе сломанный тест выглядит как успешное исправление.
Схема метода
ШАГ 1 (старт): Атакующий превращает описание уязвимости в запускаемый PoV
→ принимается, только если на уязвимом коде он СРАБАТЫВАЕТ
ШАГ 2 (фикс): Исправляющий правит код, пока все PoV из списка не заблокированы
→ затем проверка, что рабочее поведение не сломано
ШАГ 3 (атака): Атакующий получает исправленный код + diff и ищет НОВЫЙ обход
той же уязвимости → подтверждён запуском = добавляется в список
ШАГ 4 (цикл): Шаги 2-3 повторяются, пока не найден обход или не кончились раунды
ШАГ 5 (итог): Все накопленные PoV перепроверяются на финальном патче;
хоть один «ожил» → патч отклонён
ОТДЕЛЬНО: Итоговое качество меряют независимыми проверками, которых агенты не видели
Шаги выполняются в отдельных запросах или сессиях, с разными ролями. В оригинале судья — программа-оркестратор, которая запускает проверки в контейнере.
Пример применения
Задача: Вы разрабатываете интернет-магазин для селлеров Wildberries и Ozon. Селлеры загружают ZIP-архив с фото товаров, а сервис распаковывает его на сервере. Ревью нашло уязвимость: имя файла в архиве вида ../../config.py выходит за папку распаковки (zip slip). Вы просите Claude Code починить, и он добавляет проверку if not path.startswith(target_dir). Тесты зелёные. Ровно такая проверка ломалась в примере из статьи: папка /uploads/shop и «соседняя» /uploads/shop_EVIL проходят её по префиксу.
Промпт:
Ты работаешь в двух ролях строго по очереди. Репозиторий: сервис распаковки
ZIP-архивов с фото товаров, модуль upload/unpack.py. Уязвимость: path traversal
при распаковке (zip slip).
Твоя задача — написать запускаемый тест-атаку (PoV): создай злонамеренный ZIP
и скрипт, который пытается записать файл ЗА пределами папки распаковки.
Запусти его на ТЕКУЩЕМ коде и покажи вывод.
Результат только один из трёх:
REPRO — запись за пределы папки произошла;
BLOCKED — тест отработал, записи нет;
INCON — тест не отработал (ошибка сборки, таймаут, не тот вход).
INCON не считается ни успехом, ни провалом: чини сам тест.
Правь код, пока ВСЕ PoV из списка дают BLOCKED.
Затем запусти существующие тесты проекта. Сломал рабочее поведение —
откати и исправь иначе. Нельзя удалять и ослаблять PoV, чтобы получить BLOCKED.
1. Exploiter пишет PoV_0. Принимается, только если на неисправленном коде = REPRO.
Записываешь его в .
2. Patcher исправляет код под все PoV из .
3. Exploiter получает исправленный код и diff. Его задача — найти ДРУГОЙ обход
той же уязвимости: иной путь, иная кодировка, абсолютный путь, соседняя
папка с похожим именем, симлинк, другой вход в тот же код. Если PoV
даёт REPRO на исправленном коде — добавь его в , вернись к шагу 2.
4. Максимум {число_раундов} раундов. Если обход не найден за бюджет — остановись
и честно напиши: «обход не найден в рамках бюджета», а не «код безопасен».
5. В конце перезапусти ВСЕ PoV из на финальном коде.
Хоть один дал REPRO — патч отклонён.
6. Успех объявляется только по реальному выводу запуска, который ты показываешь.
Своим утверждениям «должно сработать» я не верю.
(пусто, список только растёт, ничего не удаляется)
Результат: Агент покажет цикл по раундам. Сначала PoV_0 с выводом REPRO на старом коде. Затем патч и BLOCKED. Затем попытка атакующего обойти уже новую проверку: абсолютный путь, соседняя папка с похожим именем, симлинк в архиве. Если обход найден, он попадёт в список требований и вызовет новый патч. В конце будет перезапуск всех PoV и итог: «блокирует все N найденных атак, обход в рамках бюджета не найден». Конкретные обходы зависят от вашего кода.
Почему это работает
Слабость. Агент проверяет фикс теми же данными, по которым его писал: ошибкой, трассой, одним примером атаки. Успех означает только «этот пример больше не работает». Фикс легко подгоняется под одно доказательство, а соседние пути остаются открытыми. Кроме того, агент склонен считать работу выполненной по собственному рассуждению, а не по запуску.
Сильная сторона. Одну и ту же модель можно заставить атаковать, и это получается лучше, чем «подумай, нет ли дыр». Нужен чёткий критерий: сработал реальный запуск, и обход отличается от уже найденных. Моделям хорошо удаётся перебирать варианты: другая кодировка, абсолютный путь, соседний вход.
Как метод это использует. Он меняет вопрос. Вместо «закрыт ли этот пример?» агент отвечает на «может ли кто-то пройти иначе?». Накопленные обходы остаются обязательными, поэтому следующий фикс не может «починить новое, сломав старое». Три исхода запуска не дают принять сломанную проверку за победу. Независимая итоговая проверка показывает, не обманул ли цикл сам себя.
Рычаги управления: - Число раундов → меньше для мелких уязвимостей, больше для критичных. Стоимость растёт, а находки в конце обычно редкие. - Требование «отличающийся обход» → без него атакующий повторит исходную атаку в другой обёртке. - Передача diff атакующему → он видит, что именно закрыли, и целится в границу проверки. - Условие остановки → «обход не найден в бюджете» вместо «безопасно». Это честнее и убирает ложную уверенность. - Запрет удалять и ослаблять PoV → иначе фикс «побеждает» тест, удалив его.
Быстрый старт
🚀 Вставь в чат с агентом (Claude Code, Cursor и т.п.):
Вот шаблон цикла «атакующий — исправляющий». Адаптируй под мою задачу: {опиши уязвимость и репозиторий}.
Задавай вопросы, чтобы заполнить поля.
[вставить шаблон выше]
Агент спросит, где лежит код, как запускать тесты и что считать «сработавшей атакой» в вашем случае. Это нужно потому, что весь метод держится на проверке запуском. Если непонятно, как зафиксировать REPRO или BLOCKED, цикл превращается в разговор.
Ограничения
⚠️ Нужно исполнение, а не разговор: В оригинале судья — оркестратор, который запускает проверки в контейнере. В обычном чате без запуска кода «BLOCKED» остаётся мнением модели. Метод тогда превращается в красную команду на словах, и сильная сторона пропадает.
⚠️ Не доказательство безопасности: «Обход не найден» значит только, что поиск в рамках бюджета ничего не нашёл. Исследователи прямо называют итог «ограниченным укреплением», а не гарантией.
⚠️ Исследовано на узкой задаче: Это исправление уже известных CVE в проектах на C/C++ и Java. Перенос на другие виды багов — экстраполяция, авторы его не проверяли.
⚠️ Нужна инфраструктура: Контейнер на проект, запуск проверок, защита артефактов ролей друг от друга, сравнение с эталонной версией. Всё это писал код, а не текст промпта.
⚠️ Прирост умеренный, цена выше: Лучше всего вырос показатель «закрыто сверх официального фикса». Основной показатель вырос на несколько процентов. Платите вы дополнительными раундами запуска и токенами.
⚠️ Знания из обучения: Исследователи отключили интернет и спрятали CVE, но не могут исключить, что модель помнит эти уязвимости из обучения. Часть результата может объясняться этим.
Как исследовали
Команда собрала бенчмарк VulnRepairBench из 101 проекта с известными CVE (C/C++ и Java). Агентам давали только очищенное описание уязвимости: тип слабости, трассу «источник → точка опасного вызова» и метаданные сборки. Номер CVE, официальный патч и текст эксплойта из бюллетеней убрали, интернет выключили. Базой был Claude Code за один проход: он чинил по тому же описанию без атакующей фазы.
Качество меряли независимыми PoV, которые писали люди и проверяли отдельно. «Fix-closed» ломаются на уязвимой версии и блокируются официальным фиксом. «Residual» срабатывают даже после официального патча, и это показатель укрепления сверх оригинала. Агенты этих проверок не видели, поэтому нельзя «подогнаться» под судью.
После того как патч блокировал исходный PoV, атакующий находил ещё один подтверждённый обход в 41% прогонов. Покрытие fix-closed выросло с 78,1% до 84,7%, а покрытие сверх официального патча с 42,1% до 62,5%. Четырнадцать «остаточных» дыр, найденных циклом, позже закрыли сами разработчики, иногда через 9,5 лет после первого фикса.
Удивительнее всего пример с plexus-utils: официальный патч 2016 года проверял startsWith(путь) без разделителя, и «соседняя» папка проходила. Второй фикс появился через девять лет. Агент в цикле находит такие дыры, потому что ищет границу проверки, а не повторяет показанную атаку. Для практика вывод такой: проверка «показанная атака больше не работает» не доказывает исправление, и второй, независимый шаг нужен всегда.
Примечание: текст статьи обрезан, подробности абляций и сравнения с другими системами в нём недоступны. Промпты агентов в статье не приведены, они лежат в артефакте.
Адаптации и экстраполяции
🔧 Техника: «Не доверяй ни одному зелёному» → инструкция в CLAUDE.md
Принцип можно зафиксировать в файле инструкций агента, без оркестратора. Это перенос, а не оригинальный метод:
После исправления бага или уязвимости: 1. Не считай задачу выполненной, пока не написал ещё один тест, ОТЛИЧАЮЩИЙСЯ от того, что воспроизводил баг (другой вход, другой формат, соседний путь вызова). 2. Запусти его и покажи вывод. Без реального запуска «исправлено» не пиши. 3. Если тест не запустился (ошибка сборки, таймаут) — это не «успех», а «неопределённо». 4. Все такие тесты остаются в наборе. Новые правки не должны их ломать.
🔧 Техника: два чата → независимый атакующий
Если не хочется, чтобы одна сессия подыгрывала сама себе, откройте второй чат. Передайте ему только diff и описание уязвимости, без вашего рассуждения о фиксе. Задача: «найди обход этого патча, дай запускаемый пример». Это ближе к идее разделения ролей и защиты артефактов из статьи.
Ресурсы
- P2Patch: Exploit-Guided Vulnerability Repair — Mohsen Iranmanesh, Sina Moradi Sabet и соавторы; Simon Fraser University (Канада), Amirkabir University of Technology, K. N. Toosi University of Technology, Ferdowsi University of Mashhad (Иран).
- VulnRepairBench — бенчмарк из 101 проекта с CVE, выпущен вместе со статьёй (заявлен авторами).
- Идейная основа: counterexample-guided synthesis (синтез по контрпримерам), где контрпример — запускаемая атака.
- Пример из статьи: уязвимость zip slip в
plexus-utils(Maven).
