Счёт на 180 тысяч рублей вместо привычных 15 — это не ошибка биллинга. Это классика жанра, который в 2024–2025 годах получил отдельное название — LLMjacking: угон доступа к чужим нейросетевым API и использование их за счёт владельца. По данным CrowdStrike, число подобных атак выросло в 2,5 раза за год, и это логично: аренда GPU-мощностей для тяжёлых моделей стоит дорого, а найти чужой оплаченный ключ в открытом коде или переписке — минутное дело.
В этом разборе — то же самое дело, но с технической изнанкой: как именно ключ уходит из компании, что с ним делает злоумышленник и почему один незакрытый доступ обошёлся в стоимость трёх месяцев работы всей маркетинговой команды.
Что нашли при разборе
Формально в компании всё выглядело обычным делом: сайт на связке CMS + внешний скрипт, который дергает API нейросети для генерации описаний товаров, плюс рабочий чат в мессенджере, где когда-то согласовывали интеграцию с прошлым подрядчиком. Реальная картина после аудита:
- API-ключ нейросети был вшит в открытый JS-файл на фронтенде сайта — то есть доступен любому, кто откроет исходный код страницы в браузере;
- тот же ключ лежал текстом в переписке в общем чате, доступном стажёрам и как минимум двум уже уволенным сотрудникам;
- в личном кабинете сервиса не было ни лимита расходов, ни алертов на аномальное потребление;
- ключ имел неограниченные права — без привязки к IP, без ограничения по моделям и токенам в месяц.
Как это эксплуатируется
Не нужно быть продвинутым хакером, чтобы утащить такой ключ — это рутинная работа с открытыми инструментами разведки. Вот типовая цепочка, по которой действует злоумышленник (или бывший подрядчик, знающий инфраструктуру изнутри).
Шаг 1. Разведка сайта и поиск утечек в коде
Первым делом атакующий смотрит, что отдаёт сервер и какие файлы доступны публично:
gobuster dir -u https://target-site.ru -w /usr/share/wordlists/dirb/common.txt -x js,json,env -t 50
===============================================================
Gobuster v3.6
===============================================================
/assets/js/main.js (Status: 200) [Size: 48213]
/assets/js/config.js (Status: 200) [Size: 1204]
/.env (Status: 403)
/api/generate.js (Status: 200) [Size: 812]
Дальше — просто просмотр содержимого найденных файлов через curl или браузер:
curl -s https://target-site.ru/assets/js/config.js | grep -iE "api[_-]?key|token|secret"
const GIGACHAT_API_KEY = "MjQ4YjRmM2Yt...redacted...==";
const MODEL_ENDPOINT = "https://gigachat.devices.sberbank.ru/api/v1/chat/completions";
Такие ключи в открытом фронтенд-коде находятся регулярными автоматизированными сканами — злоумышленникам не нужно целиться именно в вас, они массово гребут интернет специализированными grep-паттернами и дорками типа "api_key" filetype:js site:*.ru.
Шаг 2. Поиск утечек в истории git и мессенджерах
Если сайт разрабатывался через git-репозиторий, ключ часто остаётся в истории коммитов, даже если его потом «удалили» из актуального кода:
git clone https://github.com/somecompany/site-frontend.git
cd site-frontend
git log -p --all | grep -iE "key|token|secret" | head -50
commit a3f21e9 (author: dev@oldcontractor.ru)
+ const GIGACHAT_API_KEY = "MjQ4YjRmM2Yt...";
Или через trufflehog, который сканирует историю репозитория на предмет секретов автоматически:
trufflehog git file://./site-frontend --only-verified
Found verified result 🐷🔑
Detector Type: Generic API Key
Repo: site-frontend
Commit: a3f21e9
File: assets/js/config.js
Отдельный канал утечки — рабочие чаты в мессенджерах. Поиск по ключевым словам вроде «ключ», «api», «token» в общем чате, где не закрыты доступы бывших сотрудников, часто даёт результат быстрее, чем любой технический скан.
Шаг 3. Проверка работоспособности украденного ключа
Прежде чем запускать массовую нагрузку, атакующий проверяет ключ одним запросом:
curl -s https://gigachat.devices.sberbank.ru/api/v1/chat/completions \
-H "Authorization: Bearer MjQ4YjRmM2Yt...redacted...==" \
-H "Content-Type: application/json" \
-d '{"model":"GigaChat","messages":[{"role":"user","content":"test"}]}'
{"choices":[{"message":{"role":"assistant","content":"Здравствуйте! Чем могу помочь?"}}]}
Ключ жив, лимитов нет, ответ пришёл мгновенно — можно ставить на поток.
Шаг 4. Массовая эксплуатация
Дальше запускается скрипт, который прогоняет через чужой ключ тысячи запросов — генерацию текстов на продажу, обучение ботов, обход платных подписок для сторонних клиентов. Это тривиальный bash/python-цикл, который легко масштабируется на десятки потоков через xargs -P или очередь задач, и именно из-за такой автоматизации счёт за месяц взлетает в 10–12 раз вместо разового «попробовал и ушёл».
Что и почему сработало бы дальше
Если бы аудит не случился, сценарий развивался бы предсказуемо:
- рост расходов не остановился бы сам — без лимитов и алертов компания узнавала бы о проблеме только по факту оплаты счёта, то есть с задержкой в месяц;
- ключ мог утечь дальше — такие находки часто перепродаются или выкладываются в приватных Telegram-каналах для «шаринга» доступов к платным ИИ-сервисам;
- репутационный риск — если через угнанный ключ кто-то сгенерировал бы запрещённый контент, юридически ответственность за использование сервиса лежит на владельце ключа, то есть на компании, а не на анонимном пользователе;
- тот же вектор часто ведёт к более серьёзному компромату — раз ключ нейросети лежал в открытом коде сайта, есть большая вероятность, что там же можно найти ключи к почтовым сервисам, CRM, платёжным шлюзам. Один и тот же
config.jsредко хранит одну тайну.
Как закрыть
Работа заняла два дня и стоила в разы меньше одного «лишнего» счёта. Порядок действий воспроизводим для любой компании, использующей API нейросетей:
- Отозвать и перевыпустить ключ немедленно — в личном кабинете сервиса (GigaChat, YandexGPT, OpenAI и т.д.) старый ключ инвалидируется сразу, все текущие запросы по нему начинают падать с 401;
- Убрать ключ из фронтенд-кода — любой запрос к API должен идти через бэкенд-прокси, а не напрямую из браузера:
# Пример: ключ хранится только на сервере, фронт бьёт в свой эндпоинт # server.js (Node.js, пример) app.post('/api/generate', async (req, res) => { const resp = await fetch(process.env.GIGACHAT_ENDPOINT, { headers: { Authorization: `Bearer ${process.env.GIGACHAT_API_KEY}` }, method: 'POST', body: JSON.stringify(req.body) }); res.json(await resp.json()); }); - Хранить секреты в защищённом хранилище, а не в переменных окружения на глазок и не в чатах — Vault, AWS Secrets Manager, Yandex Lockbox или как минимум
.env, добавленный в.gitignore:echo ".env" >> .gitignore git rm --cached .env - Прогнать репозиторий сканером секретов и вычистить историю, если находки подтвердились:
trufflehog git file://./site-frontend --only-verified # при находках — переписать историю через git filter-repo или BFG Repo-Cleaner - Настроить лимиты расходов и алерты в личном кабинете провайдера нейросети — жёсткий потолок по бюджету и уведомление при отклонении от baseline-потребления (обычно это делается в разделе биллинга/квот сервиса);
- Провести ревизию доступов во всех рабочих чатах и репозиториях — закрыть доступ уволенным сотрудникам и бывшим подрядчикам, включая тех, кто формально «уже давно не работает», но чат так и не покинул;
- Регулярно сканировать периметр на утечки — не разово, а на постоянной основе, например через
nucleiс шаблонами на exposed-secrets:nuclei -u https://target-site.ru -t exposures/tokens/ -t exposures/apis/ [exposed-api-key] [medium] https://target-site.ru/assets/js/config.js
Ключ доступа к нейросети — это не техническая мелочь, это фактически банковская карта компании, привязанная к её счёту. Ключ без лимитов и уведомлений — открытая касса, к которой рано или поздно кто-то подойдёт.
Компании подключают нейросети к внутренним процессам быстрее, чем выстраивают вокруг них базовую гигиену доступа. Один незакрытый чат, один забытый JS-файл на проде — и разница между «нормальным месяцем» и «счётом в 12 раз больше» становится делом случая, а не техники защиты.
