Сегодня Telegram-бот для продажи VPN собирается за вечер. Открываешь Cursor, пишешь «сделай бота с оплатой в Stars и панелью Marzban», и через пару часов он уже принимает деньги. Только в config.py лежит токен бота, в docker-compose.yml открыт Postgres на 0.0.0.0:5432, а в истории git остался .env, который «потом удалили».
Мне стало интересно, насколько всё плохо на самом деле. Я написал свой SAST-сканер для кода, сгенерированного ИИ, и прогнал его по 3 800 публичным репозиториям: от проектов с тремя звёздами до платформ с десятками тысяч.
Цифры коротко:
Репозиториев просканировано |
~3 800 |
Боевых Telegram-ботов (VPN, магазины, Stars) |
471 |
Сырых находок high/critical на этих ботах в первой версии |
16 378 |
Из них оказались настоящими утечками |
~30 |
Правил в сканере |
21 |
Регрессионных тестов |
193 |
Внешних зависимостей |
0 |
Чужих ключей, которые я проверил или использовал |
0 |
Последняя строка самая важная, к ней я ещё вернусь. А разница между 16 378 и 30 — это, по сути, вся история проекта: как научить сканер находить настоящие дыры и молчать про всё остальное.
Что такое AigisSAST
AigisSAST — консольный сканер безопасности для кода, написанного с ИИ. Около 3 000 строк на чистом Python, без единой зависимости и без облака: код никуда не уходит.
pip install aigis-sast aigis scan
Внутри:
Python разбирается по AST, а не grep’ом. Поэтому
cursor.execute(f"... {user_id}")отличается от безобидной f-строки в логгере.21 правило под типичные ошибки ИИ: токены (OpenAI, Telegram, GitHub, AWS, Stripe и др.), пароли, строки подключения к базам, закоммиченный
.env, SQL- и shell-инъекции,eval,pickle, JWT без проверки подписи, CORS*,debug=True,verify=False, токены черезrandom, FastAPI-ручки без авторизации, XSS, секреты вVITE_*/NEXT_PUBLIC_*, root в Docker, открытые порты баз.Каждая находка объясняется по-человечески: чем опасно, как исправить, готовый код.
Секреты маскируются (
7312************), чтобы ключ не утёк второй раз через логи CI.Русский и английский: язык берётся из системы, переключается через
--lang en.
▌ bot/config.py CRITICAL AIG001 Токен или API-ключ прямо в коде (Telegram bot token) bot/config.py:12 ↗ https://github.com/you/bot/blob/3f2c1ab.../bot/config.py#L12 │ BOT_TOKEN = "7312************" чем опасно: Любой, кто увидит репозиторий, получит доступ к твоему боту... как исправить: Отзови токен у @BotFather прямо сейчас и вынеси его в .env... ──────────────────────────────────────────────────────────────── ■ critical 1 ■ high 3 □ medium 0 ■ low 2 оценка безопасности: 43/100 уровень D начни с: AIG001 bot/config.py:12
У каждой находки есть кликабельная ссылка на файл и строку на GitHub, GitLab или Bitbucket, так что до нужного места один клик.
Он ещё и чинит
aigis fix --dry-run # показать diff aigis fix # применить
Выносит ключи в .env и .env.example, дописывает .env в .gitignore, меняет verify=False на True, yaml.load на safe_load, random на secrets для токенов, выключает debug, прячет порты баз за 127.0.0.1 и сам добавляет нужные import. SQL-инъекции и JWT автоматически не трогает: там без понимания логики приложения можно только сломать.
«Я же удалил ключ»
Самая частая история: человек заметил токен, удалил его следующим коммитом и успокоился. Но старая версия файла живёт в истории git, и её видит каждый, кто сделал git clone.
$ aigis history [CRITICAL] AIG001 7312************ (Telegram bot token) bot.py · коммит 402c92b от 2026-09-14 · удалён, но остался в истории ↗ https://github.com/you/bot/commit/402c92b...
В CI подключается одной командой aigis init: находки попадают во вкладку Security → Code scanning и прямо в diff пулл-реквеста.
Раунд 1: 126 вайбкод-проектов и 2 012 «паролей»
Начал я с репозиториев с явными следами ИИ: .cursorrules, CLAUDE.md, «vibe coded», «built with Cursor/Bolt/Lovable». Медиана три звезды, обычные pet-проекты.
Первый прогон получился позорным: 2 012 захардкоженных паролей. Я сел разбирать их руками, и почти всё оказалось мусором: тестовые пароли, хеши в lock-файлах, your-key-here, ${DB_PASSWORD}, переменные вроде hashed_password и ACCESS_TOKEN_EXPIRE_MINUTES, переводы, где password значит «Пароль».
После доработки осталось 138 находок, а настоящие ключи не потерялись: каждый исправленный случай я закрепил тестом. Ручная разметка показала точность по секретам примерно 60–70%. Это честная цифра, а не маркетинговая.
Раунд 2: 49 гигантов open source
Потом я пошёл к взрослым: 49 популярных ИИ-платформ, low-code-сервисов и админок с тысячами звёзд.
Правило |
Было |
Стало |
|---|---|---|
Все находки |
3 709 |
1 422 |
AIG002 пароли |
1 525 |
392 |
AIG003 URL базы с паролем |
276 |
4 |
AIG021 FastAPI без авторизации |
299 |
71 |
AIG001 токены известных форматов |
164 |
28 |
Почти все 28 оставшихся «токенов» оказались тестовыми и примерными. Крупные проекты за секретами следят. Отсюда же вылезла проблема оценки: в огромной кодовой базе даже аккуратный проект набирает сотни находок и получает F. Пересчёт с поправкой на размер стоит у меня в планах.
Раунд 3: 2 292 маленьких проекта — боты, ИИ, VPN
Дальше были маленькие свежие проекты: Telegram-боты, ИИ-приложения, сайты, VPN-боты и панели (Marzban, 3x-ui, WireGuard, Xray). Сканировались и текущий код, и вся история git.
После ручного отсева осталось около 50 проектов с доступами, похожими на настоящие: ключ Google service account вместе с .env, пароли от облачных MongoDB и TiDB, закоммиченные .env у VPN-ботов, десятки токенов ботов прямо в коде, OpenAI- и Google-ключи.
Раунд 4: 471 бот, который приносит деньги
Учебные проекты мне были уже неинтересны. Я искал ботов, которыми реально пользуются: VPN-магазины, магазины цифровых товаров, боты с оплатой в Stars, CryptoBot и ЮKassa. Фильтр: не форк, коммиты за последний год, минимум два признака живого использования (ссылка на работающего бота, подключённая оплата, деплой, админка), никаких tutorial/template.
Шаг |
Репозиториев |
|---|---|
Кандидатов из 41 поискового запроса |
1 514 |
Прошли фильтр «боевой бот» |
473 |
Просканировано (код + история git) |
471 |
Похожие на настоящие утечки после ручного разбора |
31 |
— секрет лежит в коде прямо сейчас |
10 |
— только в истории git |
22 |
Что нашлось: токены ботов прямо в коде, токен CryptoBot вместе с адресом и паролем панели 3X-UI в «примере» .env, GitHub-токен в истории, дамп конфига VPN-панели с приватными ключами. И популярная open-source панель, где токен бота зашит в исходники, поэтому он уезжает каждому, кто её ставит.
Самое ценное в этом раунде было не про чужих ботов, а про мой сканер. 16 378 находок, из них настоящих около 30. Шум давали:
переводы в VPN-панелях:
"newPassword" = "Nueva Contraseña"— это подпись к полю;вендорный
vue.js, гдеemptySlotScopeToken = "_empty_"похож на токен только по имени;example-значения в OpenAPI ("password": "secret");тесты, шаблоны
.env.j2, тестовые PEM-ключи,postgres://postgres:postgres@localhost.
Нашёлся и настоящий баг: aigis history держал весь git log -p в памяти, падал по OOM на больших историях и при этом молча писал пустой отчёт с кодом 0. Для сканера безопасности хуже бага не придумаешь: семь репозиториев чуть не записались в «чистые».
Я исправил всё. История теперь читается потоком, а при сбое git сканер честно падает с кодом 2. На одной популярной VPN-панели было 37 находок, стало 3, на другой было 29, стало 7. Настоящий зашитый токен бота при этом в отчёте остался, ради этого всё и делалось.
Раунд 5: 879 проектов вслепую
Последняя проверка самая честная: я прогнал свежую версию ещё по 879 проектам, которые сканер раньше не видел и под которые я его не подгонял. Находок снова хватило: токены ботов и API-ключи в коде и в истории git, закоммиченные .env, строки подключения к базам с паролем.
Названий проектов в статье нет ни в одном раунде, и это сделано специально.
Почему я не проверял ни одного ключа
Меня постоянно спрашивали: «Почему бы не дёрнуть getMe у Telegram и не узнать, живой ли токен?»
Потому что это уже использование чужого доступа. Запрос с чужим токеном аутентифицирует тебя как владельца бота, и неважно, что метод «только читает». Нормальный security-ресёрч устроен так: нашёл ключ по формату и контексту, сообщил владельцу файл и строку (без самого ключа), а проверку и отзыв оставил ему. Поэтому все цифры выше значат «похоже на настоящий ключ», а не «ключ работает».
Я ни разу не входил ни в один чужой сервис, бот, базу или панель и не пользовался найденными ключами. Сканер только читает публичный код и маскирует секреты. Проект я делаю для пользы сообщества: чтобы ИИ-код попадал в прод с меньшим числом дыр, а владельцы узнавали об утечках раньше злоумышленников.
Что я понял
ИИ ошибается не случайно, а по шаблону. Секреты в коде, f-строки в SQL, CORS
*,debug=True, ручки безDepends(...)— одни и те же ошибки в тысячах проектов.Найти легко, промолчать трудно. Больше всего времени ушло не на то, чтобы сканер находил, а на то, чтобы он не орал на всё подряд. Из 193 тестов большая часть как раз про то, чего находить не надо.
Удалённый ключ — не удалённый ключ. Попал в публичный репо, значит отзывай, а не удаляй коммитом.
Тихий сбой хуже ложной тревоги. Сканер, который упал и сказал «всё чисто», опаснее, чем отсутствие сканера.
Попробовать
Всё описанное выше есть в версии 0.5.0:
pip install aigis-sast aigis help # шпаргалка по всем командам aigis scan # текущий код aigis history # секреты в истории git aigis fix # автоисправление aigis init # подключить к GitHub Actions
Код открыт под MIT: https://github.com/jasperBLCK/AigisSAST. Если сканер ошибся на вашем проекте в любую сторону, заведите issue с примером: каждое ложное срабатывание у меня превращается в регрессионный тест.
Проект я буду развивать дальше: новые правила, меньше ложных срабатываний, поддержка других языков. Буду рад любой поддержке: звезда на GitHub, issue, PR или просто отзыв.
И вопрос к вам: насколько полезной показалась разработка и поставили бы вы её в свой CI? Напишите в комментариях, мне правда важно.
nullc0de
-del