3,583 papers
arXiv:2610.20245 72 14 окт. 2026 г. FREE

P2Patch: после каждого исправления уязвимости второй агент пытается его обойти

КЛЮЧЕВАЯ СУТЬ
Фикс, который блокирует показанную атаку, ещё не чинит уязвимость: после такого фикса второй агент находил рабочий обход в 41% прогонов. Метод P2Patch позволяет проверять исправление уязвимости на прочность. Один агент чинит код, второй пытается сломать именно этот фикс. Фишка: атакуй не саму уязвимость, а готовый патч. Атакующий видит исправленный код и ищет другой обход: иной путь, иную кодировку, соседнюю папку. Каждый найденный обход остаётся в списке требований навсегда, и финальный патч обязан блокировать все накопленные атаки.
Адаптировать под запрос
⚡

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).

📋 Дайджест исследования

Ключевая суть

Фикс, который блокирует показанную атаку, ещё не чинит уязвимость: после такого фикса второй агент находил рабочий обход в 41% прогонов. Метод P2Patch позволяет проверять исправление уязвимости на прочность. Один агент чинит код, второй пытается сломать именно этот фикс. Фишка: атакуй не саму уязвимость, а готовый патч. Атакующий видит исправленный код и ищет другой обход: иной путь, иную кодировку, соседнюю папку. Каждый найденный обход остаётся в списке требований навсегда, и финальный патч обязан блокировать все накопленные атаки.

Принцип работы

Цикл из пяти шагов. 1. Атакующий пишет запускаемый пример атаки (PoV, proof of vulnerability). Его принимают, только если на уязвимом коде он срабатывает. 2. Исправляющий чинит код, пока все примеры из списка не заблокированы. 3. Атакующий получает исправленный код и разницу в коде (diff). Он ищет новый обход. 4. Обход подтверждён запуском? Он попадает в список, и всё повторяется с шага 2. 5. В конце все атаки перезапускают на финальном патче. Ожила хоть одна, и патч отклонён. Меняется сам вопрос: не «закрыт ли этот пример?», а «может ли кто-то пройти иначе?» У запуска три исхода: атака сработала, заблокирована и неопределённо (сбой сборки, таймаут). Их нельзя смешивать. Иначе сломанный тест выглядит как победа. Это как замок: после ремонта к нему зовут не слесаря, а вора, который видит, что именно поменяли.

Почему работает

Агент проверяет фикс теми же данными, по которым его писал: ошибкой, трассой, одним примером атаки. Успех значит лишь «этот пример больше не работает». Фикс подгоняется под одно доказательство, а соседние пути остаются открытыми. Перебирать варианты модели умеют: кодировки, абсолютные пути, соседние входы. Но им нужна чёткая задача. «Найди другой обход и докажи запуском» работает лучше, чем «подумай, нет ли дыр». Атакующий видит diff и целится точно в границу новой проверки. Накопленные обходы остаются обязательными, поэтому следующий фикс не чинит новое, ломая старое. Теперь без розовых очков. Основной показатель вырос всего на несколько процентов. Заметнее всего вырос показатель «закрыто сверх официального фикса». Платить придётся лишними раундами и токенами. Ещё авторы не исключают, что модель помнит эти CVE (публичные номера известных уязвимостей) из обучения. Часть результата может объясняться этим.

Когда применять

Исправление уязвимостей → особенно когда фикс — короткая проверка пути, ввода или прав доступа, а тесты зелёные на одном примере атаки. Агент должен реально запускать код (Claude Code, Cursor с терминалом). НЕ подходит для обычного чата без запуска кода. Там «заблокировано» остаётся мнением модели, и сила метода пропадает. Это не доказательство безопасности. «Обход не найден» значит только «в рамках бюджета не нашли». Исследовали известные CVE в проектах на C/C++ и Java. Перенос на другие виды багов авторы не проверяли, это догадка.

Мини-рецепт

1. Дай агенту запуск: контейнер или терминал, где он выполняет код и показывает вывод. Без этого дальше нет смысла.
2. Заведи список требований: туда попадают все атаки. Он только растёт, удалять из него нельзя.
3. Раздели роли: <роль>атакующий пишет запускаемую атаку, <роль>исправляющий чинит. Лучше в отдельных запросах или сессиях.
4. Введи три исхода: REPRO (атака прошла), BLOCKED (заблокирована), INCON (тест сломался). INCON не считается ни победой, ни провалом. Чини тест.
5. Требуй другой обход: иной путь, кодировка, абсолютный путь, симлинк. Иначе атакующий повторит старую атаку в новой обёртке.
6. Запрети читерство: атаки нельзя удалять и ослаблять, чтобы получить BLOCKED. Существующие тесты проекта прогоняй после каждого патча.
7. Поставь бюджет раундов: 2-3 для мелочи, больше для критичного. Стоп-фраза: «обход не найден в рамках бюджета», а не «код безопасен».
8. Закрой цикл: в конце перезапусти все атаки на финальном коде. Ожила хоть одна — патч отклонён.

Примеры

[ПЛОХО]: `Почини zip slip в upload/unpack.py` Агент добавит `if not path.startswith(target_dir)`, тесты позеленеют. Но папка `/uploads/shop_EVIL` проходит такую проверку по префиксу. Атака с `../../config.py` заблокирована, соседняя папка открыта. [ХОРОШО]: `Работай в двух ролях по очереди. Атакующий: напиши ZIP и скрипт, который пишет файл за пределы папки распаковки, запусти на текущем коде и покажи вывод (REPRO, BLOCKED или INCON). Исправляющий: правь upload/unpack.py, пока все атаки из списка требований дают BLOCKED, потом прогони тесты проекта. Затем атакующий получает исправленный код и diff и ищет ДРУГОЙ обход: абсолютный путь, соседнюю папку с похожим именем, симлинк. Нашёл — добавь в список и чини снова. Максимум 3 раунда. В конце перезапусти все атаки. Не нашёл обход — пиши «не найден в рамках бюджета», а не «безопасно». Верю только реальному выводу запуска.` [ПЛОХО]: `Проверь, нет ли в моём патче дыр` Без запуска это размышления, а не проверка. Модель ответит «выглядит надёжно» и успокоится. [ХОРОШО]: `Напиши запускаемую атаку на мой патч, которая отличается от исходной. Покажи вывод запуска. Если атака сработала — это обход, добавь его в список требований.`
Источник: P2Patch: Exploit-Guided Vulnerability Repair
ArXiv ID: 2610.20245 | Сгенерировано: 2026-10-15 08:00

Проблемы LLM

ПроблемаСутьКак обойти
Исправление закрывает только тот пример, который показалиДаёшь модели ошибку, трассу или один пример сбоя. Она чинит ровно это. Пример больше не падает, тесты зелёные. Но соседние входы, пути и кодировки остаются открытыми. Проверка идёт теми же данными, по которым писали фикс. Поэтому ловить нечего. Выглядит как успех, хотя причина не устранена. Так бывает с любыми багами: парсеры, проверка ввода, регулярки, права доступаНе проси просто «почини и проверь». Добавь второй шаг: отдельная роль ищет другой способ вызвать ту же проблему на уже исправленном коде. Найденное добавляется в обязательные проверки. Схема в блоке «Методы»
Модель считает работу сделанной по своим рассуждениям, а не по запускуМодель пишет «должно работать» и закрывает задачу. Код она не запускала или не показала вывод. Сломанная проверка при этом выглядит как победа. Например, тест не собрался, а модель решила, что атака заблокированаТребуй показывать реальный вывод запуска. Введи три исхода: «сработало», «заблокировано», «неопределённо». Неопределённый исход не считается успехом. Тест при этом чинят, а не принимают

Методы

МетодСуть
Цикл «починил → попытался сломать именно этот фикс» — закрывает соседние дырыРазбей работу на две роли. Запускай их по очереди, лучше в отдельных запросах. Шаги: 1) Атакующий пишет запускаемый пример проблемы. Он принимается, только если на старом коде срабатывает. 2) Исправляющий чинит код, пока все примеры из списка не заблокированы. Потом гонит обычные тесты проекта. 3) Атакующий получает новый код и разницу со старым. Он ищет ДРУГОЙ обход: иной путь, иную кодировку, соседний вход. 4) Обход подтверждён запуском — он попадает в список. Возврат к шагу 2. 5) В конце все примеры из списка запускаются на финальном коде. Хоть один сработал — патч отклонён. Синтаксис: , , (список только растёт). Исходы: REPRO / BLOCKED / INCON. Почему работает: вопрос меняется. Вместо «закрыт ли этот пример?» модель отвечает на «может ли кто-то пройти иначе?». Разница в коде показывает атакующему границу новой проверки. Он целится именно в неё. Накопленный список не даёт чинить новое, ломая старое. Правила против обмана: нельзя удалять или ослаблять примеры атак. Нужно требование «отличающийся обход», иначе атакующий повторит старую атаку в другой обёртке. Задай максимум раундов: меньше для мелочей, больше для критичного. Когда да: безопасность, проверка ввода, парсинг, любые правки, где важны соседние случаи. Когда нет: в чате без запуска кода. Там BLOCKED остаётся мнением модели, и метод превращается в разговор. Не подходит для задач без чёткого критерия «сработало / не сработало»

Тезисы

ТезисКомментарий
Конкретная атака с проверкой запуском работает лучше, чем «подумай, нет ли дыр»Общий вопрос «найди проблемы» даёт общие слова. Модель легко соглашается, что всё в порядке. Конкретная задача с критерием («дай запуск, который реально ломает код, и он отличается от прежних») заставляет перебирать варианты. Модели хорошо удаётся такой перебор: другая кодировка, абсолютный путь, соседний вход. Применяй: вместо «проверь на уязвимости» пиши «напиши запускаемый тест, который обходит этот фикс. Покажи вывод. Он должен отличаться от прежних»
«Обход не найден» не значит «безопасно»Поиск ограничен числом раундов. Он находит не всё. Если модель пишет «код безопасен», это ложная уверенность. Почему важно: вывод о безопасности делается из отсутствия находки, а такое отсутствие доказывает мало. Применяй: задай условие остановки в запросе: «если обход не найден за N раундов, напиши: обход не найден в рамках бюджета. Не пиши, что код безопасен». Это верно для любой проверки перебором: тесты, поиск ошибок, аудит
📖 Простыми словами

P2Patch: Exploit-Guided Vulnerability Repair

arXiv: 2610.20245

Когда нейросеть чинит уязвимость в коде, она тупо подгоняет решение под один пример. Если популярная модель видит баг, она лепит костыль ровно против того сценария, в который её ткнули носом. В итоге формально всё зелёное, но меняешь один символ в запросе — и сервис снова ломают. Создатели архитектуры P2Patch решили эту проблему радикально: заставили две модели жестоко мочить друг друга.

Это как заколотить дыру в заборе первой попавшейся доской и радостно уйти пить кофе. Хозяин спокоен, пока хитрый сосед не видит, что в двух метрах правее забора вообще нет. Обычный ИИ мыслит как ленивый сторож: закрыл ровно ту щель, о которую споткнулся. А чтобы система реально стала пуленепробиваемой, нужно нанять второго взломщика, который сразу полезет искать свежие лазейки в свежем заборе.

В схеме P2Patch в бой идут две роли: агент-атакующий и агент-исправляющий. Первый пишет рабочий эксплойт (PoV), второй патчит код, пока атака не захлебнётся. Дальше самое мясо: атакующий получает обновлённый код и обязан найти новый обход той же уязвимости. Каждый найденный трюк навсегда падает в копилку тестов, а финал оценивает не болтовня ИИ, а реальный запуск кода на скрытых проверках.

Авторы тестировали схему на баге Zip Slip при распаковке архивов, где тупая проверка префикса папки легко пропускает взломщика в соседний каталог. Но принцип универсален. Тот же состязательный цикл закрывает дыры в API, парсерах и авторизации — везде, где разработчики слепо верят сгенерированным костылям. Глупые патчи больше не прокатят.

Главный вывод: никогда не давай ИИ проверять собственную домашку. Модель охотно убедит тебя, что код в безопасности, если не натравить на неё агента-параноика. Кто продолжит внедрять слепые автопатчи, будет долго гадать, куда утекли базы клиентов. Надежный код рождается только в непрерывном спарринге двух сетей.

Работа с исследованием

Адаптируйте исследование под ваши задачи или создайте готовый промпт на основе техник из исследования.

0 / 2000
~0.5-2 N-токенов ~10-30с
~0.3-1 N-токенов ~5-15с