Коротко. Мы делаем много аудитов приложений, собранных в Cursor, Claude Code, Bolt, Lovable и в долгих сессиях с ChatGPT. Кодовые базы разные, а вот находки — почти всегда одни и те же. Одиннадцать проблем всплывают снова и снова, примерно в порядке убывания вреда: захардкоженные секреты и учётные данные, отсутствие валидации ввода (поверхность для инъекций), аутентификация, которая формально есть, но не проверяет запрос, нулевое покрытие тестами, отсутствие обработки ошибок на несчастливом пути, N+1-запросы и производительность, отданная на волю случая, устаревшие зависимости с непропатченными известными CVE, дублирование переменных и функций, нет единой архитектуры, сложное асинхронное состояние, скатившееся в callback hell вместо стримов, и код, написанный без учёта окружения развёртывания. Ничего экзотического. Всё это предсказуемо — и всё чинится без переписывания с нуля. Это инженерное дополнение к нашему гайду для основателей о выводе AI-прототипа в продакшен; а если хотите передать это нам — именно этим занимается аудит AI-кода.


Что такое аудит — и что не аудит

Аудит — это не переписывание с нуля и не приговор тому, стоило ли вообще собирать это на AI. Это структурированное чтение работающей кодовой базы, отвечающее на один вопрос: что произойдёт, когда её впервые встретит реальный злоумышленник, реальный всплеск трафика или реальное изменение через полгода? Находки ниже — не гипотетические категории из чек-листа: каждая из них — то, что мы находили, обезличивали и чинили в реальном проекте. Мы ссылаемся на пример отчёта об аудите и на сервис аудита AI-кода на протяжении всего материала, потому что именно там происходит исправление; этот пост — доказательство того, зачем оно нужно.

Почему находки повторяются

AI-инструменты для написания кода оптимизируют ровно одно: кратчайший путь к коду, который запускается. Это правда полезно — идея превращается в рабочий прототип за часы. Но «работает в демо» и «поддерживаемо и безопасно в продакшене» — разные цели, и разрыв между ними удивительно одинаков от проекта к проекту.

Причина структурная. Модель, генерирующая код, видит узкое окно контекста и не помнит решений, которые приняла три файла назад. Она не может протестировать то, что только что написала, у неё нет представления ни о вашем запасе денег, ни о ваших требованиях к безопасности, и нет стимула удерживать кодовую базу связной во времени. Поэтому она каждый раз делает локально оптимальный выбор — а сумма локально оптимальных выборов даёт код, который работает сегодня и сопротивляется любому изменению завтра.

После достаточного числа аудитов сбои сводятся к одним и тем же одиннадцати категориям. Вот они.


1. Захардкоженные секреты и учётные данные

Это находит каждый аудит, и это ровно тот запах кода, что стоит реальных денег: .env-файлы, закоммиченные в репозиторий, API-ключи и учётные данные баз данных, захардкоженные прямо в коде, токены сторонних сервисов, вшитые в клиентские бандлы, которые уходят в каждый браузер, загружающий приложение. Один утёкший ключ OpenAI или Stripe может за часы намотать тысячи долларов на несанкционированных списаниях — или прямо отдать злоумышленнику ваше хранилище данных.

Почему AI так делает. Захардкодить ключ — это работает сразу; настроить менеджер секретов или подстановку переменных окружения — нет, а у модели нет причины предпочитать более медленный путь, если быстрый тоже «запускается». Кратчайший путь к рабочей фиче почти никогда не безопасный.

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

2. Нет валидации ввода — поверхность для инъекций

SQL-инъекции, XSS и встречаются в AI-коде повсеместно, и корень проблемы всегда один и тот же: пользовательский ввод попадает прямиком в запрос, шаблон или промпт LLM без всякой валидации и санитизации по пути. Один уязвимый эндпоинт может скомпрометировать всю базу данных или позволить злоумышленнику манипулировать тем, что делают ваши собственные AI-фичи.

Почему AI так делает. Валидация — это второй запрос, которого модели никто не задавал. Она генерирует код, удовлетворяющий счастливому пути из промпта — «возьми сообщение пользователя и сохрани его» — а счастливый путь никогда не упоминает, что нужно отклонить.

Как мы это чиним. На каждой границе, где в систему входит непроверенный ввод, появляется явная валидация и параметризованные запросы или шаблоны вместо конкатенации строк. Для AI-фич это отдельно означает, что построение промпта само по себе — граница, подверженная инъекциям, а не только слой базы данных.

3. Аутентификация, которая формально есть, но не проверяет запрос

AI-приложения часто реализуют аутентификацию на поверхностном уровне — экран логина есть, и он работает — но бэкенд на самом деле никогда не проверяет права на конкретный запрос. API-эндпоинты принимают всё, что до них долетает. Административные маршруты доступны без проверки роли. Пользователь A может увидеть данные пользователя B, просто подменив ID в URL — небезопасная прямая ссылка на объект, живущая в продакшене.

Почему AI так делает. «Добавь страницу логина» и «проверь, что именно этому запросу разрешено трогать именно эту запись» — разные задачи, и модель решает ту, о которой её спросили. Аутентификация видна в демо; пробелы в авторизации невидимы, пока их кто-то не проэксплуатирует.

Как мы это чиним. Мы проверяем каждый эндпоинт на то, кому реально разрешено его вызывать, а не на то, что подразумевает экран логина, и добавляем недостающие проверки авторизации на уровне запроса — проверки владения записью, проверки роли на административных маршрутах и rate limiting на всё, что можно закидать скриптом.

4. Нет тестов — каждый деплой становится лотереей

Самая частая находка: тестов нет вообще. Не тонкий набор, не флакающие тесты — ноль. Приложение проверяли кликами по нему, и это вся страховочная сетка.

Это незаметно ровно до того момента, когда становится катастрофой. Без тестов нельзя узнать, сломало ли изменение что-то ещё, иначе как выкатив его и дождавшись жалобы пользователя. Каждый деплой превращается в ручной регресс, который никто на самом деле не проводит, поэтому рефакторинг становится страшным, обновления зависимостей пропускаются, а кодовая база костенеет — не потому что код плох, а потому что никто не решается его трогать.

Почему AI так делает. Сгенерировать фичу и сгенерировать тесты к ней — это два разных запроса, и второй никто не сделал. Модель охотно напишет тесты, если попросить, но предоставленная сама себе она выдаёт счастливый путь и останавливается.

Как мы это чиним. Мы не гонимся за 100% покрытием в первый день. Мы добавляем тонкий слой там, где он окупается сильнее всего: smoke-тест, что приложение стартует, тесты вокруг логики с деньгами и авторизацией, и регрессионный тест на каждый баг, который чиним по ходу аудита. Уже это превращает деплой из лотереи в рутину.

5. Нет обработки ошибок на несчастливом пути

Приложение работает ровно так, как в демо — пока сеть не отваливается, сторонний API не отвечает по таймауту, а пользователь не делает ничего неожиданного. В момент, когда что-то из этого случается, симптомы уродливые: необработанное отклонение промиса роняет весь запрос, упавший вызов API оставляет UI навечно висеть на спиннере, исключение показывает пользователю сырой стектрейс вместо сообщения, которое хоть что-то для него значит.

Почему AI так делает. Счастливый путь — это то, что описал промпт и что прогнало демо. Обработка ошибок — это защитный код для ситуаций, которые модели никто не просил представить, и он добавляет строки, не делая демо ни капли более впечатляющим, — поэтому его пропускают первым при неявном лимите времени.

Как мы это чиним. Мы проходим по каждому внешнему вызову — API, база данных, файловая система — и добавляем ветку отказа: ретраи с backoff там, где ретрай помогает, фолбэк или понятное состояние ошибки там, где не помогает, и логирование, которое говорит, что реально произошло, а не общее «что-то пошло не так». Цель в том, чтобы сбой стороннего сервиса лишь частично ухудшал работу приложения, а не укладывал его целиком.

6. N+1-запросы и производительность, отданная на волю случая

Экран списка, который мгновенно загружается с десятью строками в разработке, ползёт с десятью тысячами в продакшене. Классическая причина — N+1-запрос: один запрос за списком, затем отдельный запрос на каждую строку за связанными данными, так что экран, который должен стоить одного обращения к базе, стоит сотен. Отсутствующие индексы, неограниченные выборки без пагинации и загрузка целых объектов, когда отображается одно-два поля — обычные спутники. Наш разбор баз данных и индексов объясняет механику того, почему это медленно и как выглядит здоровый план запроса.

Почему AI так делает. Паттерн N+1 — самый очевидный способ написать цикл, и он даёт корректный результат — у модели нет обратной связи, которая сказала бы, что количество запросов важно, пока кто-то не измерит это на реальном объёме данных, чего демо с горсткой строк никогда не делает.

Как мы это чиним. Мы профилируем реальные паттерны запросов при реалистичном объёме данных, схлопываем цепочки N+1 в джойны или батчевую загрузку, добавляем недостающие индексы и ставим пагинацию или лимиты на всё, что возвращает неограниченный набор. Обычно это самое результативное исправление производительности за весь аудит: один плохой экран списка способен съедать большую часть времени загрузки страницы.

7. Устаревшие зависимости и дрейф CVE

npm audit или его аналог выдаёт стену известных уязвимостей в тот момент, когда кто-то наконец его запускает — потому что раньше никто не запускал. Пакеты закреплены на той версии, что была актуальна, когда AI-инструмент разворачивал проект, транзитивные зависимости, которые никто напрямую не выбирал, несут свои собственные CVE, и нет процесса, который бы сообщил, когда выходит патч.

Почему AI так делает. Модель выбирает пакет, решающий текущую задачу, и идёт дальше; сопровождать ваш проект она не будет, и ничто не заставит её вернуться к этому выбору позже. Гигиена зависимостей — это часть сопровождения, а в генерации фичи нет ничего, что запускало бы сопровождение.

Как мы это чиним. Мы запускаем сканирование уязвимостей зависимостей, патчим или заменяем всё с известным эксплойтом и настраиваем процесс — хотя бы простое плановое сканирование — чтобы всё это снова тихо не поехало сразу после окончания аудита.

8. Дублирование переменных и функций

Откройте AI-кодовую базу и поищите один и тот же хелпер форматирования дат. Часто вы найдёте его три-четыре раза — каждый чуть отличается, потому что каждый сгенерирован изолированно под экран, которому он понадобился. То же с правилами валидации, API-клиентами, денежной арифметикой и константами конфигурации.

Дублирование не просто уродливо; это бомба замедленного действия для корректности. Когда логику нужно изменить — новое правило налога, исправленный баг округления, обновлённый эндпоинт — придётся найти каждую копию. Одну вы пропустите. И вот уже две части приложения расходятся в том, в чём должны были совпадать, и это расхождение — следующий инцидент в продакшене.

Почему AI так делает. Модель редко ищет в существующем коде хелпер, который могла бы переиспользовать. С её точки зрения дешевле перегенерировать функцию инлайн, чем найти и импортировать ту, что уже есть. Каждая генерация локально разумна; в сумме получается дрейф.

Как мы это чиним. Мы находим кластеры почти идентичного кода, выносим единый источник правды и пускаем через него все точки вызова. Это одна из самых результативных чисток в большинстве аудитов: она сжимает кодовую базу и убирает целые классы багов «починили здесь, но не там».

9. Нет единой архитектуры

Когда видишь такое впервые — это настоящий шок. Два экрана в одном проекте написаны так, будто их делали две разные команды: один тянет данные в компоненте, другой — через сервисный слой; один хранит состояние одним способом, следующий — совершенно иначе; именование, структура папок и обработка ошибок меняются от фичи к фиче. Нет хребта.

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

Почему AI так делает. У модели нет устойчивой картины «как устроено это приложение». Каждый запрос — чистый старт, поэтому она хватается за любой паттерн, который подходит под этот конкретный запрос. За жизнь проекта это даёт лоскутное одеяло — каждый кусок разумен по отдельности, целое бессвязно.

Как мы это чиним. Мы выбираем одну архитектуру, подходящую проекту — не догматичную, а подходящую — и инкрементально сводим к ней кодовую базу, чтобы разработчик, разобравшийся в одной фиче, мог предсказать, как работает следующая.

10. Состояния на стримах избегаются — прямиком в callback hell

Это технически самый интересный сбой и тот, что тихо ломает самые сложные фичи. Код, сгенерированный AI, склонен избегать стримовых и реактивных моделей состояния в пользу императивных колбэков. Вместо того чтобы смоделировать «это значение меняется во времени, и UI на него реагирует», он навешивает колбэк, который дёргает другой колбэк, который ставит флаг, который запускает третий — и получается callback hell.

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

Почему AI так делает. Императивные колбэки — самый частый паттерн в обучающих данных модели и самый простой для генерации по кусочку. Реактивные и стримовые модели требуют держать в голове весь конечный автомат сразу — ровно то, в чём генератор с ограниченным контекстом слабее всего.

Как мы это чиним. Мы выявляем фичи со сложным состоянием и перестраиваем их слой состояния правильно — как стримы или реактивную модель, подходящую стеку, — чтобы UI стал функцией состояния, а не кучей колбэков, гоняющихся друг за другом.

11. Нет представления об окружении развёртывания

Модель пишет код так, будто он будет работать одним процессом на одной машине — потому что изнутри промпта это единственное окружение, которое она видит. Она не знает, сколько инстансов будет запущено, какие управляемые сервисы уже есть и как маршрутизируется трафик. Поэтому она по умолчанию берёт простейшую топологию — и этот дефолт тихо ломается, как только приложение разворачивают по-настоящему.

Симптомы всегда одни и те же. Состояние, живущее в памяти процесса — кеш, сессии, счётчики rate-limit — отлично работает на одном инстансе и незаметно расходится, как только за балансировщиком поднимается вторая реплика. Фоновые задачи срабатывают на каждом инстансе вместо одного, поэтому письмо уходит трижды.

Почему AI так делает. У неё нет картины вашей инфраструктуры. Она не знает, что у вас уже есть Redis, очередь сообщений и объектное хранилище — поэтому переизобретает их в памяти. Топология деплоя — это ровно тот контекст, который промпт не может вместить.

Как мы это чиним. Мы составляем карту реального деплоя и переносим общее состояние туда, где ему место: кеш, сессии и локи в Redis или базу, файлы в объектное хранилище, периодические задачи на нормальный планировщик или очередь. В итоге код масштабируется горизонтально.


Как мы их находим

Каждая находка выше начинается с автоматического прохода и заканчивается человеком, читающим код. Автоматизация — статический анализ, сканирование зависимостей и секретов, профилирование расходов на API — это то, что делает AI-ускоренный аудит быстрым: она закрывает категории, механически поддающиеся обнаружению (находки 1, 4, 6 и 7 выше всплывают почти сразу), так что время наших инженеров концентрируется на категориях, требующих суждения — логике авторизации, архитектуре и том, действительно ли конкретный путь ошибки важен для вашего продукта. Ни одна половина не работает сама по себе: одна только автоматизация упускает всё, что требует понимания, для чего вообще нужен код, а одно только ручное ревью не масштабируется на реальную кодовую базу за неделю. Полный разбор процесса — на странице сервиса аудита AI-кода.

Что вы получаете в отчёте

Находки не приходят сырым списком. Каждая ранжирована по критичности — критическая, высокая, средняя, низкая — с понятным объяснением риска, proof of concept где применимо, и конкретной рекомендацией по исправлению — той же формы, что и каждая находка выше. Если хотите увидеть формат до того, как на что-то решиться, запросите обезличенный пример отчёта об аудите — ту же структуру отчёта, что получается в реальном проекте, без данных, идентифицирующих клиента.

Что стоит за всеми этими паттернами

Сделайте шаг назад — и у одиннадцати находок обнаружится один общий корень: AI оптимизирует каждую генерацию локально, а кодовую базу глобально не оптимизирует никто. Управление секретами, валидация ввода, авторизация, тесты, обработка ошибок, производительность запросов, гигиена зависимостей, дедупликация, архитектура, моделирование состояния и учёт окружения развёртывания — всё это свойства всей системы. Они не могут возникнуть по одному промпту за раз, потому что ни один промпт не видит целого. Именно этот разрыв закрывает человеческое ревью.

Обнадёживает то, что ничего из этого не значит, будто фундамент, собранный на AI, пропал зря. Фичи работают; продукт настоящий. Не хватает соединительной ткани — и добавить её куда быстрее, чем переписывать с нуля.


Частые вопросы

В десятках кодовых баз, написанных AI, почти каждый раз повторяются одиннадцать проблем: захардкоженные секреты и учётные данные, отсутствие валидации ввода, аутентификация, которая формально есть, но не проверяет запрос, ноль автоматических тестов, отсутствие обработки ошибок на несчастливом пути, N+1-запросы и производительность, отданная на волю случая, устаревшие зависимости с известными CVE, сильное дублирование переменных и функций, нет единой архитектуры по проекту, сложное асинхронное состояние реализовано как callback hell вместо стримов, и код написан без учёта окружения развёртывания. Конкретный код от проекта к проекту разный, но эти одиннадцать категорий всплывают снова и снова.
Захардкодить ключ — это работает сразу, а настроить менеджер секретов или подстановку переменных окружения — нет, и у модели нет причины предпочитать более медленный, правильный путь, когда быстрый тоже запускается. В результате — .env-файлы в репозитории, ключи захардкожены в коде, токены вшиты в клиентские бандлы. Всё, что хоть раз попало в git, нужно считать уже скомпрометированным и ротировать, а не просто удалять — старое значение остаётся действительным в каждом клоне и в каждой истории коммитов.
Валидация — это второй запрос, который модели явно никто не задавал. Она генерирует код, удовлетворяющий счастливому пути из промпта, а счастливый путь никогда не упоминает, что нужно отклонить, поэтому пользовательский ввод часто попадает прямиком в запрос, шаблон или промпт LLM без санитизации между ними. Это прямая причина SQL-инъекций, XSS и промпт-инъекций, которые всплывают почти в каждой проверенной нами AI-кодовой базе.
Потому что написать фичу и написать тесты к ней — это два разных запроса, и второй обычно не делают. AI-инструменты заточены под кратчайший путь к коду, который запускается, а это счастливый путь без набора тестов. Модель напишет тесты, если попросить, но сама по себе она выдаёт фичу и останавливается, оставляя каждый будущий деплой без страховки.
Модель редко ищет в существующем коде хелпер, который могла бы переиспользовать. С её точки зрения дешевле перегенерировать функцию инлайн под нужный экран, чем найти и импортировать ту, что уже есть. Каждая генерация локально разумна, но в итоге одна и та же логика скопирована несколько раз с мелкими различиями, и это становится проблемой корректности в момент, когда логику нужно изменить.
Код от AI склонен избегать стримовых и реактивных моделей состояния в пользу императивных колбэков. Для простых экранов это нормально, но сложное состояние вроде многошаговых форм с кросс-валидацией, дебаунсом или оптимистичными апдейтами сваливается в callback hell. Классический симптом — форма, которая почти работает: ошибка пропадает не в тот момент, двойной тап отправляет дважды. Моделирование состояния как стрима это чинит.
Самая частая причина — паттерн N+1: один запрос за списком, затем отдельный запрос на каждую строку за связанными данными, так что экран, который должен стоить одного обращения к базе, стоит сотен. В разработке с горсткой строк выглядит корректно и всплывает только на реальном объёме данных, потому что ничто в процессе генерации не измеряет количество запросов. Отсутствующие индексы и неограниченные выборки без пагинации — обычные спутники.
Потому что у модели нет картины того, как приложение развёрнуто. Она пишет код для одного процесса на одной машине, поэтому держит состояние в памяти и пишет файлы на локальный диск. На одном инстансе это работает, но ломается, как только приложение параллелится за балансировщиком: кеши в памяти, сессии и счётчики rate-limit расходятся между репликами, а фоновые задачи срабатывают на каждом инстансе вместо одного. Чинится переносом общего состояния в подходящие сервисы, чтобы приложение масштабировалось горизонтально.
Нет. Все одиннадцать находок чинятся на месте. Секреты убираются и ротируются, ввод валидируется на каждой границе, проверки авторизации ставятся на каждый нужный эндпоинт, тесты добавляются там, где окупаются сильнее всего, пути отказа получают нормальную обработку ошибок, N+1-запросы схлопываются в батчевую загрузку, зависимости патчатся, дублированная логика выносится в единый источник правды, кодовая база сводится к одной согласованной архитектуре, фичи со сложным состоянием получают нормальный реактивный слой, а общее состояние переносится в подходящие сервисы. Это сохраняет рабочий фундамент, который дал AI, и чинит только недостающую соединительную ткань, что куда быстрее и дешевле переписывания.

Получите находки по своей кодовой базе

Если у вас приложение на AI и вы узнаёте хотя бы одну из этих одиннадцати проблем — вы не отстаёте, вы ровно там, где оказывается почти любая AI-кодовая база. Решение не в переписывании; это сфокусированный аудит, который добавляет соединительную ткань, которую AI не смог.

Прогоните свою кодовую базу через аудит AI-кода, запросите пример отчёта об аудите, чтобы сначала увидеть формат, или закажите бесплатную оценку — мы скажем, какая из одиннадцати проблем несёт для вас самый большой риск, что нужно, чтобы её починить, и дадим фиксированную смету, а не догадку.