AI пишет, AI проверяет: почему уязвимости появляются пачками и как понять, какие из них настоящие

Wait 5 sec.

За пару лет вопрос «умеет ли модель писать код» перестал быть интересным. Умеет. Ассистент в IDE стал такой же частью рабочего окружения, как линтер, а агент, который берёт задачу целиком и сам ходит по репозиторию, — уже не демо с конференции, а рабочий инструмент. Спорить осталось не о способностях, а о последствиях.Одно из последствий обсуждают заметно меньше остальных. Код стали писать быстрее, а проверять — с той же скоростью, что и раньше. Ревью делает человек. Разбор находок статического анализатора делает человек. Решение «это настоящая уязвимость или ложное срабатывание» — тоже человек, и стоит оно вполне конкретных минут рабочего времени. В итоге узкое место переехало: генератор кода работает круглосуточно, а проверка — по восемь часов пять дней в неделю.Дальше всё развивается предсказуемо. Раз разобрать поток руками невозможно, на разбор ставят языковую модель — ту же технологию, которая этот поток и создала. AI пишет код, AI ищет в нём уязвимости, AI решает, какие из находок настоящие. И остаётся вопрос: как понять, что модель не ошиблась, если проверять её ответ, вообще-то, тоже некому.Эту проблематику подробно разбирали на OFFZONE — конференции по практической кибербезопасности, которая прошла в Москве 20–21 августа. Среди тем докладов — применение ИИ для поиска уязвимостей и анализа результатов, автоматизация задач специалистов и новые риски, которые возникают вместе с этими возможностями. Два выступления особенно точно продолжили разговор о том, что происходит, когда в цепочку поиска и проверки уязвимостей всё активнее включается AI. Мы попросили их авторов подробнее прокомментировать эту тему.Дмитрий Абрамов занимается уязвимостями, которые приносит генеративный код: что именно ломается, когда код пишет агент. Юрий Туманов отвечает за вторую половину задачи — за разбор находок. Он строит на локальных языковых моделях триаж сработок SAST: статический анализатор просматривает код и выдаёт список подозрительных мест (это и есть сработки), а триаж — разбор этого списка на настоящие уязвимости и ложные срабатывания.Каждый из них говорил про свою часть работы. Но в двух местах они, не сговариваясь, сказали почти одно и то же — об этом в конце.Находок стало больше. Но не только из-за уязвимостейНачну с оговорки, без которой дальше легко скатиться в панические настроения.Да, поток находок вырос в несколько раз. Но объяснять это только дырявым машинным кодом неправильно, и Дмитрий сразу это проговаривает:«Рост находок в несколько раз больше. Но я бы это связал, в первую очередь, с ростом количества строк кода из-за скорости написания кода, перестройки паттернов сканеров и периода внедрения новых сканеров».То есть часть роста — это не новая опасность, а новая видимость. Строк стало больше, потому что их стали быстрее писать. Правила сканеров перестроили. Подключили инструменты, которые раньше в эти места не смотрели.Но команде, которая разбирает эту очередь, от такого объяснения не легче: откуда бы ни взялся рост, вручную столько находок всё равно не обработать. И дело не только в количестве — изменилось ещё и то, как эти находки приходят.Три механизма, из-за которых дефекты идут пачкамиЭто, по словам Дмитрия, главное практическое отличие генеративного кода. Раньше уязвимость была разовым событием: кто-то один раз ошибся в одном месте. Теперь ошибка тиражируется.Первый механизм — шаблонный. Агент подобрал способ решения и применяет его везде, где видит похожую задачу:«Агент применяет один шаблон ко всем похожим местам — если шаблон дефектный, дефект тиражируется».Второй — конфигурационный. Ошибка лежит не в коде, а уровнем выше: «если ошибка сидит в правилах или в контекстном файле, то она размножается на весь репозиторий». Одна неточная инструкция отрабатывает в каждой задаче, за которую агент берётся.Третий механизм Дмитрий Абрамов называет самым коварным:«Агент берёт за образец существующий код. Если в репозитории уже есть уязвимый паттерн, он его подхватывает и воспроизводит. Получается самоусиление: одна старая ошибка становится стандартом де-факто».Практический вывод отсюда простой. Чинить находки по одной бессмысленно. Если вы видите два десятка однотипных срабатываний, у них почти наверняка один источник: шаблон, правило или пример, который уже лежит в репозитории. Разбираться нужно с источником, а не с каждой строчкой в отчёте.Что ушло, что пришлоНабор дефектов заметно изменился. Меньше стало синтаксических ошибок и простых логических багов. Отдельная история — SQL-инъекции: их стало «заметно меньше просто потому, что модель почти всегда тянет ORM». Не из соображений безопасности, а потому что так написано в большинстве примеров, на которых модель училась.Вместо них выросли пути эскалации привилегий, архитектурные дефекты, SSRF (когда сервер можно заставить сходить по адресу, который выбрал злоумышленник), отсутствие CSRF-защиты и security-заголовков. Плюс, как формулирует Дмитрий, «отсутствие понимания архитектуры приложения» — код, который сам по себе корректен, но не учитывает, как устроена система вокруг.Причину он называет одну:«Но все эти ошибки чаще всего проскакивают из-за недостаточности контекста, передаваемого агенту».Часть проблем лечится буквально этим. Заголовки и CSRF в его команде почти исчезли после того, как добавили то, что они назвали «ИБ-контекстом». Модель не отказывалась делать безопасно — ей просто не сказали, что от неё этого ждут.А вот ролевая модель и бизнес-логика контекстом не лечатся. Здесь нужны ручные проверки или специфичные автотесты, а значит, снова человеческое время.Кстати, он поправил саму постановку вопроса про слепые зоны:«Слепыми называть неправильно, скорее было бы правильно — невнимательные».Разница есть. Слепая зона — то, чего инструмент не видит в принципе. Невнимательность — то, что исправляется правильно поставленной задачей.Ошибки не в коде, а в поведении агентаЕсть категория проблем, которая не попадёт ни в один SAST, потому что это вообще не про код.«Лишний вызов инструмента, преждевременное завершение задачи, „починил“ тест вместо кода, сфабрикованный отчёт о прогоне — как у Replit. Это не свойство кода, это свойство процесса, и статические анализаторы к нему не приспособлены по определению».Эта мысль пригодится дальше. Модель, которая уверенно рапортует об успехе, — проблема не только на стороне написания кода, но и на стороне его проверки.Разбирать некому. Ставим на разбор модельМасштаб задачи у Юрия Туманова выглядит так: в агрегаторе, куда стекаются сработки всех анализаторов, накоплено больше миллиона записей. Решение одного аппсека — специалиста по безопасности приложений — по одной сработке стоит около пятнадцати рублей рабочего времени. Дальше можно умножать.Напрашивается очевидное: спросить у модели, настоящая это уязвимость или ложное срабатывание. Спросить действительно можно.«Спросить можно, и ответ придёт мгновенно — складный, развёрнутый, уверенный».У него в докладе есть слайд, который так и называется: «Складно ≠ доказано».Почему прямой вопрос не работаетПричин три.Уверенность модели ничего не значит. Модель на семь миллиардов параметров — класс «умной автодополнялки» — на прямой вопрос подтверждает почти половину сработок и выдаёт целые серии с уверенностью 1.0.«Само-оценка модели без внешней калибровки театральна, опираться на неё нельзя».Формулировка вопроса определяет ответ. Куда модель натаскали при обучении, туда она и копает. Универсальная рамка «найди источник и сток» — то есть место, где данные попадают в приложение, и место, где они используются в опасной операции, — хорошо работает на цепочечных дефектах. И не работает там, где никакой цепочки нет: захардкоженный пароль или слабый алгоритм шифрования видно по одной строке.«Универсальная рамка „найди источник и сток“ на дефектах-„свойствах“ хоронит реальное: на одном моём замере неудачная постановка вопроса похоронила 140 реальных сработок из 363».У прямого вопроса нет метрик. Без размеченного эталона — набора сработок, по которым заранее известны правильные ответы, — неизвестно ни какая доля подтверждений оказалась правдой, ни какую долю реальных дефектов модель вообще поймала. Остаётся верить ей на слово.Здесь стоит остановиться. У Дмитрия дефект проскакивает, потому что агенту недодали контекста. У Юрия модель хоронит реальную сработку, потому что ей задали не тот вопрос и не показали нужный кусок кода. В обоих случаях дело не в модели.Модель предполагает, правила решаютГлавное, что стоит понять про систему Юрия: последнее слово в ней принадлежит не модели. Порядок такой — модель предполагает, улики подтверждают, правила проверяют. Сам по себе вердикт модели не закрывает ничего.Второе — сработки разделены на два сорта, и доказываются они по-разному.Уязвимость-свойство. Опасен сам факт в коде: пароль в исходниках, слабый хеш, слишком открытые права. Вопрос ровно один — настоящий секрет или заглушка. Никакого пути данных здесь нет и быть не должно. Таких сработок в потоке подавляющее большинство, около 93%.Для них выстроена лестница проверок, и модель на ней — лишь одна из ступеней. Сначала работает алгоритмика без всякой LLM: ключ в формате боевого AWS — сразу подтверждаем, файл сгенерированный или из SBOM — сразу ложное срабатывание. Это снимает больше половины шума и не стоит ни одного обращения к модели. Дальше вопрос уходит модели, но задаётся по сути факта. Потом больше сотни детерминированных правил проверяют её ответ и могут вердикт поправить. Потом пороги по классам: слабый ответ превращается в Unknown. Последняя ступень — человек. Отдельным правилом запрещена сама формулировка «не вижу пути атаки» как причина закрытия: именно на ней система и теряла те самые 140 сработок из 363.Уязвимость-путь. Здесь опасен маршрут данных: SQL-инъекция, XSS, обход каталогов. Вопрос другой — дошли ли данные атакующего до опасного вызова и была ли по дороге защита. Вот для этого типа и сделан evidence gate.Evidence gate: перечисли факты — вердикт выведется самИдея в том, чтобы не спрашивать у модели вердикт вообще. Сначала она обязана заполнить таблицу фактов по жёсткой схеме: найти настоящий sink — строку, где реально выполняется опасный вызов (сканер её часто теряет); перечислить каждую переменную запроса — откуда пришла, какой строкой, была ли по дороге защита; выписать список незащищённых переменных.И только потом вердикт вычисляется из этой таблицы механически. Список не пуст — уязвимость подтверждена. Пуст, и происхождение всех переменных известно — ложное срабатывание. Улики повреждены — Unknown, пусть смотрит человек. Переопределять этот результат собственной интуицией модели прямо запрещено.Держится всё на двух подпорках. Первая — порядок полей в ответе: сначала дословные цитаты из кода, потом вывод. Схема зафиксирована на уровне формата, пропустить шаг физически нельзя. Одна только перестановка полей подняла долю верно закрытых ложных срабатываний больше чем вдвое.Вторая — проверка цитат. Каждая цитата должна дословно найтись в том коде, который модели выдали. Не нашлась — ответ бракуется целиком. Проверка не лишняя: в первом же эксперименте десятая часть вердиктов ссылалась на строки, которых в выданном фрагменте вообще не было. Модель «вспоминала» несуществующий код.«Придумать вывод легко, придумать проверяемую улику сложно. Фактам — да, поэзии — ни-ни».Unknown в этой схеме — не сбой, а штатный ответ и часть защиты. Система обязана его выдать, если не определён источник или сток трассы — цепочки, по которой анализатор проследил путь данных через код; если виден только фрагмент без окружающего кода; если непонятно, тест это или бой; если не видно значение переменной. На главном замере через Unknown человеку ушло 370 сработок, и реальных уязвимостей среди них не оказалось ни одной. То есть механизм отработал ровно так, как задумывался.И снова всё упирается в контекст. Чаще всего система отвечает «недостаточно данных» по одной причине: трасса слишком короткая. Модель видит место, где переменная попадает в опасный вызов, но не видит, откуда эта переменная пришла и проверяли ли её по дороге. Лечится это не увеличением окна и не моделью побольше, а доставкой недостающего кода. Юрий пробовал 72B через внутренний портал и получил обратный результат:«Дисциплина важнее размера».Решение, какому классу дефектов доверить автозакрытие, принимают не на глаз, а по результатам замера. Для каждого класса отдельно берут около сотни сработок, по которым уже есть решения живых аппсеков, и сравнивают: что по этим же сработкам сказала модель и что сказали люди. Совпало — класс допускают до автомата, нет — он продолжает ходить к человеку.Дальше система работает на реальном потоке. Там она закрывает чуть больше половины очереди, и ни одной пропущенной уязвимости за ней пока не зафиксировали. Расплата за такую осторожность — около двухсот перестраховок: сработок, по которым система могла бы вынести вердикт сама, но на всякий случай передала их человеку.Почему локальноПриватность здесь не аргумент в споре, а входное условие: код, трассы и содержимое агрегатора за периметр не выходят. Облачный вариант отпал сразу.Но у локального стенда есть и собственные плюсы, главный из них — воспроизводимость. Фиксированные веса и квантизация — сжатие модели до размера, который помещается в видеокарту, — дают повторяемые вердикты, а на этом держится еженедельная калибровка.«Облачная модель обновляется без спроса, и вчерашний замер сегодня уже ни о чём».Со стоимостью тоже всё сходится: одна игровая RTX 4080 разбирает около восьми тысяч сработок в сутки за электричество.История про токенСамая показательная часть доклада— не про метрики.«Боевой GitLab-токен, модель с уверенностью 1.0 обозвала его „placeholder in test configuration“ — заглушкой в тестовом конфиге, — поверила пути файла и похоронила. Реальный секрет, закрытый автоматом».Доверие команды после такого падает мгновенно:«Тысяча правильных вердиктов проходит незамеченной, одну галлюцинацию запоминают все и надолго».Вернуть его можно, но только механизмами — словам после такого уже не верят. Что сработало: ошибка превращается в регресс-тест, и команда видит, как система после неё изменилась. У каждого вердикта есть журнал с уликами и обоснованием, любой можно поднять и разобрать спустя месяц. Политику для классов с секретами ужесточили: там модель больше не закрывает сработки в одиночку. В поток подсадили канареек — намеренно внедрённые реальные дефекты, проверка на полноту. Стресс-тест показал, что часть из них закрывалась, и после него появились алгоритмические проверки самой логики решения.«Доверие к автомату устроено как доверие к коллеге: ошибаться можно, прятать ошибки нельзя. Система, которая сама показывает промахи и после каждого меняется, доверие возвращает. Система, которая уверенно бормочет „всё хорошо“, теряет его навсегда».Где проходит границаОбоим спикерам был задан один и тот же вопрос: что бы вы не стали автоматизировать в ближайший год, даже если технически уже можно? Ответы стоит прочитать подряд.Дмитрий Абрамов:«У меня довольно чёткая граница, и она проходит не по сложности задачи, а по обратимости последствий и по возможности проверить результат. Не стал бы автоматизировать применение фиксов без человека. Черновик патча — отлично, автомерж в прод — нет, особенно в авторизации и криптографии».Дальше в его списке — агент в промышленной среде, автоматическое закрытие тасок и автоматический перевод сработки в статус ложной. Последнее он оставляет как мнение модели, а не как решение.Юрий Туманов отвечает про свою часть работы, но приходит к тому же. Автомат работает там, где улика видна прямо по артефакту, а полноту можно проверить канарейками. Цепочечные классы — SQL-инъекции, XSS, path traversal, всё, где нужно доказать путь данных через приложение, — идут к человеку: именно там небольшие модели выдают уверенные галлюцинации, а уверенный неверный вердикт обходится дороже, чем честный ответ «недостаточно данных». Необратимые действия без человека — ротация и отзыв секретов, блокировки, автопатчи с автомержем — тоже нет. И отдельно:«Выбор допустимого риска и разметка эталона — решение людей с числами на столе. Эталон размечают люди: отдай его модели — и система начнёт сверяться сама с собой, по кругу».Дмитрий и Юрий занимаются разными вещами. Один смотрит, как AI пишет код, второй — как AI этот код проверяет. Но границу автоматизации оба провели в одном месте, и правило у них получилось одинаковое: автоматизировать можно то, что потом можно проверить. Всё остальное — необратимые действия и всё, что зависит от контекста, которого не видно, — модель готовит, фильтрует и предлагает, но решает человек.Совпало и кое-что конкретное. Оба назвали одно и то же действие, которое автоматизировать нельзя: перевод сработки в статус ложной без человека. Хотя со стороны именно оно выглядит самым безобидным — подумаешь, закрыли лишнюю строчку в отчёте.Логика тут простая. Если система ошиблась в другую сторону и отдала человеку лишнюю сработку, это стоит нескольких минут его времени, и ошибку заметят. Если она посчитала ложной настоящую уязвимость, не заметит никто — пока эту уязвимость не найдёт кто-то другой.