TL;DR
SBVUL — двухфазная схема работы кодового агента, которая ищет в репозитории конкретные эксплуатируемые строки кода вместо общей оценки «функция уязвима или нет». Два отдельных агента с инструментами чтения и поиска по файлам (Read, Grep, Glob) последовательно находят подозрительные операции и проверяют их по графу вызовов.
Когда одну модель просят сразу найти уязвимости во всей функции, она либо тонет в ложных срабатываниях, либо указывает на место будущего патча (проверку if), а не на саму опасную операцию. Более того, если тот же агент в одном диалоге и выдвигает гипотезу, и проверяет её, он попадает в ловушку подтверждения: оправдывает собственную догадку косвенными уликами вместо строгой трассировки данных.
Метод разделяет поиск и проверку между двумя изолированными сессиями без общей истории диалога. Первый агент («Охотник») широко исследует функцию, вызывающий и вызываемый код и выписывает список строк-кандидатов, а второй агент («Верификатор») с чистым контекстом проверяет поток управления и данных по каждому кандидату и относит все недоказанные допущения в категорию uncertain (не определено).
Схема метода
Шаги выполняются в двух отдельных запросах (сессиях агента) с доступом к инструментам поиска по коду (Read, Grep, Glob), но без общей истории чата:
ШАГ 1 (Сессия 1 — Охотник): Анализ функции + поиск callers/callees по репозиторию → JSON-список строк-кандидатов [{"line", "code", "reason"}]
ШАГ 2 (Сессия 2 — Верификатор): Трассировка потоков данных и проверок для каждого кандидата с нуля → JSON-вердикты [confirmed / ruled out / uncertain]
ШАГ 3 (Правило фильтрации): Отсечь ruled out и uncertain → оставить в отчёте только confirmed
Пример применения
Задача: Вы проводите аудит безопасности бэкенд-сервиса приёма вебхуков оплаты от российских платёжных шлюзов (ЮKassa и СБП) перед релизом и хотите с помощью агента в Claude Code или Cursor найти реальные дыры в обработке входящих платежей без десятков пустых предупреждений.
Промпт:
Запрос 1 (Сессия 1 — поиск кандидатов):
Ты — специализированный эксперт по поиску уязвимостей в коде. Твоя задача — определить, содержит ли целевая функция `process_sbp_webhook` в файле `services/payment/webhook_handler.py` эксплуатируемую уязвимость.
## Фаза 1 — Исследование и поиск строк-кандидатов
Начни с чтения и разбора исходного кода целевой функции. Используй инструменты поиска по файлам и проанализируй:
- Структуру и граф вызовов: найди все места, откуда вызывается функция (callers), и посмотри, что она делает с аргументами.
- Источники заражения и опасные стоки (taint sources & sinks): проследи, откуда приходят параметры (внешние JSON-поlya вебхука, заголовки, параметры запроса) и доходят ли они до опасных операций (SQL-запросы, вызовы команд, запись в баланс, десериализация, работа с памятью и индексами).
- Логические уязвимости: пропущенная валидация подписи, нарушения авторизации, состояния гонки или устаревшие ссылки после вызовов с побочными эффектами.
Строка, которую ты указываешь для каждого кандидата, должна быть именно тем оператором, выполнение которого совершает небезопасное действие (а не местом, где не хватает проверки).
Остановись, когда кандидаты исчерпаны. Выведи JSON-массив — по одному объекту на кандидата:
[{"line": <номер строки числом>, "code": "<краткий фрагмент кода>", "reason": "<почему строка может быть эксплуатируемой>"}]
Запрос 2 (Сессия 2 — чистый чат, передаём только код функции и JSON из Фазы 1):
Ты — специализированный эксперт по верификации уязвимостей.
Вот список строк-кандидатов из Фазы 1 для функции `process_sbp_webhook` (`services/payment/webhook_handler.py`):
[вставить JSON из Фазы 1]
## Фаза 2 — Проверка каждого кандидата
Подтверди или опровергни каждого кандидата с помощью инструментов чтения файлов и поиска по репозиторию:
- Перечитай точные строки вокруг каждого кандидата в контексте файла.
- Найди другие места вызова функции и проверь, не валидируются ли данные выше по стеку (в middleware, роутере или вызывающем коде).
- Проверь определения типов, схем данных и граничных условий.
Вердикт обязан опираться только на фактически собранные инструментами доказательства, а не на допущения. Если для уязвимости нужно условие, которое ты не проверил по коду, ставь вердикт `uncertain`.
Для каждого кандидата укажи: `confirmed` (подтверждено), `ruled out` (опровергнуто) или `uncertain` (не определено). Прекращай расследование кандидата, как только доказательства стали однозначными.
Выведи JSON-массив:
[{"line": <номер строки>, "verdict": "confirmed | ruled out | uncertain", "explanation": "<доказательство с путём потока данных>"}]
Результат:
В первой сессии агент пройдёт по вызовам обработчика вебхука и соберёт 3–5 подозрительных строк в JSON (например, прямой вызов начисления средств на счёт или запрос в базу данных). Во второй сессии новый агент проверит middleware и вызывающие модули: отбросит кандидатов, защищённых проверкой HMAC-подписи на уровне роутера (ruled out), пометит спорные места без явных улик как uncertain и оставит confirmed только там, где неочищенный параметр реально доходит до опасной операции.
Почему это работает
Когда LLM ищет баги в один проход, она смешивает две противоположные задачи: широкую генерацию идей (высокая полнота) и строгую отбраковку (высокая точность). Если заставить модель быть осторожной сразу, она пропустит реальные дыры. Если попросить искать всё подряд, разработчик утонет в ложных тревогах.
Сильная сторона современных моделей с инструментами — умение целенаправленно ходить по файлам репозитория (Grep, Read), когда им задана узкая цель. Если разделить процесс на два независимых этапа без общей памяти, второй агент не видит черновых рассуждений первого. Он получает только сухую гипотезу (номер строки и причину) и проверяет её как внешний аудитор.
Ключевой механизм защиты от галлюцинаций здесь — принудительная категория uncertain и требование привязки к конкретной строке опасной операции (sink). Модели запрещено угадывать: если она не нашла в коде подтверждения того, что атакующий контролирует переменную, она обязана списать кандидата в uncertain, который при финальной фильтрации отбрасывается.
Рычаги управления:
- Инструкция по строке-мишени → требуй указывать именно строку выполнения опасной операции, иначе модель будет указывать на if-проверки и заголовки функций.
- Правило для uncertain → для критичных модулей можно просматривать uncertain глазами, а для быстрой проверки в CI оставлять только confirmed.
- Лимит шагов поиска → ограничь число вызовов инструментов (например, до 50–80 шагов на фазу), чтобы агент не уходил в бесконечный обход дерева вызовов.
Шаблон промпта
Ты — специализированный эксперт по поиску уязвимостей и ошибок в коде на {язык_или_стек}. Твоя задача — определить, содержит ли функция `{имя_функции}` в файле `{путь_к_файлу}` эксплуатируемую уязвимость.
## Phase 1 - Explore & Hunt for Candidate Statements
Начни с чтения и анализа исходного кода целевой функции. Используя инструменты чтения и поиска по репозиторию, проанализируй:
- Structure & call graph: найди вызывающие функции (callers) и посмотри, что целевая функция делает с аргументами и где используется возвращаемое значение (callees).
- Taint sources & sinks: проследи, могут ли параметры формироваться из недоверенного внешнего ввода и доходить без проверки до опасных операций ({типы_опасных_операций}).
- Logic-level patterns: пропущенная валидация, проверки прав доступа, устаревшие ссылки после вызовов с побочными эффектами, ошибки работы с размерами и индексами.
Строка, которую ты указываешь для каждого кандидата, должна быть именно тем оператором, выполнение которого совершает небезопасное действие.
Выведи JSON-массив — по одному объекту на кандидата:
[{"line": <номер строки числом>, "code": "<краткий фрагмент кода>", "reason": "<объяснение уязвимости>"}]
---
[Запускается в НОВОЙ сессии агента без истории Phase 1]
Вот список кандидатов из Phase 1 для функции `{имя_функции}` (`{путь_к_файлу}`):
{json_кандидатов_из_фазы_1}
## Phase 2 - Confirm Each Candidate Statement
Подтверди или опровергни каждого кандидата с помощью доступных инструментов поиска и чтения кода:
- Перечитай точные строки вокруг кандидата в контексте файла.
- Найди другие места вызова функции, определения типов, макросов и проверок границ в соседнем коде.
- Проследи поток управления и данных от источника ввода до строки-кандидата.
Вердикт обязан опираться только на фактически собранные доказательства, а не на предположения. Если ты не проверил предположение по коду, кандидат получает статус `uncertain` (пустой результат поиска сам по себе не является доказательством).
Для каждого кандидата запиши вердикт: `confirmed`, `ruled out` или `uncertain`. Прекращай проверку кандидата, как только доказательства однозначны.
Выведи JSON-массив:
[{"line": <номер строки>, "verdict": "confirmed | ruled out | uncertain", "explanation": "<доказательство с путём потока управления или данных>"}]
Что подставлять:
- {язык_или_стек} — язык проекта (например, C/C++, Python/FastAPI, Go, TypeScript).
- {имя_функции} и {путь_к_файлу} — проверяемая функция и файл в репозитории.
- {типы_опасных_операций} — характерные для стека стоки (например, memcpy/free для C++, SQL/eval/subprocess/платёжные транзакции для веба).
- {json_кандидатов_из_фазы_1} — JSON-ответ первого агента, который передаётся во вторую сессию.
🚀 Быстрый старт — вставь в чат:
Вот шаблон двухфазного аудита кода SBVUL. Адаптируй под мою задачу: [твоя задача].
Задавай вопросы, чтобы заполнить поля.
[вставить шаблон выше]
LLM спросит, на каком языке написан проект, какую функцию или модуль нужно проверить и есть ли у вас агент с доступом к файлам (например, Claude Code или Cursor) — потому что методу нужно настроить список опасных операций под ваш стек и развести инструкции на два отдельных запуска. Она возьмёт паттерн из шаблона и адаптирует под задачу.
Ограничения
⚠️ Ложные тревоги на уже пропатченном коде: Даже после второй фазы модели ошибочно подтверждают уязвимости в значительной доле уже исправленных функций. Если защитная проверка написана нестандартно или разнесена по нескольким функциям, агент может её не распознать и выдать ложный
confirmed.
⚠️ Слепота к уязвимостям из-за отсутствующего кода: Метод ищет конкретную опасную строку внутри функции. Если суть бага в том, что в функции вообще забыли написать действие (например, не сбросили флаг состояния), а взлом происходит совсем в другом модуле, привязка к строкам не сработает.
⚠️ Высокий расход токенов и времени на глубоком анализе: Каждая фаза делает десятки вызовов инструментов чтения и поиска по репозиторию. Включение максимального режима рассуждений (thinking) на слабых или медленных моделях может приводить к таймаутам без прироста точности.
Как исследовали
Команда проверила подход на 200 парах реальных функций C/C++ из бенчмарка PrimeVul Paired (охвачено 104 открытых проекта и 45 типов уязвимостей CWE), где каждая пара состоит из уязвимой функции и её же версии после реального патча безопасности. Сначала авторы с помощью Opus 5.5 и двойной ручной выверки разметили в этих 200 функциях 380 реально эксплуатируемых строк и обнаружили важный факт: в 56% уязвимых функций патч вообще не трогает саму опасную строку, а просто вставляет проверку выше по коду — поэтому старые бенчмарки, сверявшие ответ модели со строками из git diff, оценивали поиск заплатки, а не поиск самой дыры. Затем исследователи прогнали двухфазного агента на четырёх моделях (Qwen 3.8 27B, DeepSeek V4.1 Flash, Claude Sonnet 5.5 и Claude Opus 5.5) через LangChain и Claude Code Agent SDK, причём мало просто угадать номер строки — судья на базе LLM сверял, правильно ли агент объяснил корневую причину бага. В итоге Фаза 1 находила реальные уязвимые строки в 56–77% функций, а Фаза 2 с Opus 5.5 правильно классифицировала 42,0% пар (против 33,8% у лучшего предыдущего метода VulTriage и 18,6% у мультиагентных дебатов VulTrial), а по критерию с проверкой объяснения CORRECT достигла 75,5% против 17,2% у VulInstruct. При этом исследование честно вскрыло слабую зону всех современных LLM: в 23–61% уже исправленных функций модели всё равно находили «подтверждённую» уязвимость, так что вердикты агента нужно использовать как точечный навигатор для ревьюера, а не как безошибочный блокиратор релиза.
Адаптации и экстраполяции
1. Адаптация техники
🔧 Техника: Поиск первопричины продакшен-багов по логу ошибки → точная локализация сломанного оператора
Замените поиск уязвимостей на поиск строки, порождающей некорректное состояние данных (например,
NaNв расчёте скидки или дублирование заказа):## Phase 1 - Hunt for Bug-Origin Statements Найди все строки в функции и вызываемых методах, где изменяется поле `{имя_поля}` или вычисляется итоговая сумма. Выпиши каждую строку-кандидат в JSON. ## Phase 2 - Verify Data Flow В новой сессии проверь по коду репозитория, какие из кандидатов могут получить `{ошибочное_входное_значение}` в обход существующих проверок. Непроверенные гипотезы помечай как `uncertain`.
2. Экстраполяция
В проектах с Claude Code или Cursor оформи Фазу 1 и Фазу 2 как двух субагентов (
.claude/agents/vuln-hunter.mdи.claude/agents/vuln-verifier.md). Главный агент при ревью пул-реквеста сначала вызываетvuln-hunterна изменённых функциях, забирает чистый JSON-массив кандидатов и передаёт его вvuln-verifier, который стартует с пустым контекстом и возвращает разработчику только подтверждённые (confirmed) строки с трассировкой.
Ресурсы
Исследование: SBVUL: Statement-Based Vulnerability Detection (arXiv:2611.06135) Репликационный пакет и промпты: https://doi.org/10.5281/zenodo.23881951 Авторы: Martin Pinzger, Katharina Stengg, Christian Macho (University of Klagenfurt), Stefan Schöberl (Software Competence Center Hagenberg), Víctor Mayoral Vilches (Alias Robotics)
