API без подарков прохожим
Ключ лежал в .env. После сборки он переехал в браузер
Мы собрали маленькое Vite-приложение с фальшивым API-ключом и нашли его в готовом JavaScript. Вот почему .gitignore не спасает клиентский код и куда на самом деле убирать секрет.

Разработчик убрал .env из Git, проверил .gitignore и выдохнул. Потом опубликовал сайт. Через минуту любой посетитель мог скачать JavaScript и забрать API-ключ. Git ничего не слил. Сборщик честно выполнил приказ.
Это особенно легко пропустить в приложении, которое написал AI-агент. Кнопка работает, модель отвечает, в репозитории секретного файла нет. Снаружи всё прилично. Ключ тем временем уже уехал в браузер вместе с цветами кнопки и текстом приветствия.
01
Мы сделали самый аккуратный небезопасный проект
Взяли пустое Vite-приложение и положили в .env.production фальшивое значение. Оно специально не похоже на настоящий ключ и не может оплатить ни одного запроса. Переменную назвали с префиксом VITE_, а файл добавили в .gitignore.
Клиентская кнопка брала это значение и подставляла в заголовок запроса. Затем мы запустили обычную production-сборку. Приложение не публиковали и никуда не обращались: для опыта хватило готовой папки dist.
В репозитории ключа нет. В сайте он есть. Эти две фразы отлично уживаются.
02
Сборка заняла 53 миллисекунды. Ключ нашёлся в первой строке
Production build · безопасная канарейка
Результат локального опыта, не рисунок с придуманными цифрами
Минифицированный файл весил около килобайта. Фальшивый ключ лежал там открытым текстом рядом с адресом API и телом запроса. Не понадобились взлом, доступ к серверу или чтение исходного репозитория. Достаточно того файла, который браузер обязан скачать для работы страницы.
03
.env — это ящик стола. Браузерный bundle — витрина
Файл .env прячет значение от Git, если его действительно исключили из коммита. Он не определяет, куда значение попадёт после сборки. Vite прямо предупреждает: переменные с префиксом VITE_ встраиваются в клиентский код и не должны содержать секреты.
В Next.js действует похожее правило для NEXT_PUBLIC_. Префикс буквально означает «отправить в браузер». Название файла может быть сколь угодно таинственным. После сборки посетитель получает готовое значение, потому что иначе его собственный JavaScript не смог бы выполнить запрос.
Обфускация тоже не сейф. Можно переименовать переменную, разрезать строку на куски или спрятать её в библиотеке. Браузеру всё равно придётся собрать заголовок перед отправкой. Значит, человек с доступом к своему браузеру сможет увидеть итог.
04
Секрет заканчивается там, где начинается браузер
Браузер
Отправляет только пользовательский текст и получает готовый ответ.
Ваш сервер
Проверяет пользователя, лимит и размер запроса. Здесь хранится ключ.
TokenTool API
Получает серверный запрос и возвращает результат выбранной модели.
Сервером может быть обычный backend, serverless-функция или edge-функция. Марка не важна. Важно, что код выполняется вне устройства пользователя, а ключ не попадает ни в HTML, ни в JavaScript, ни в сетевой запрос браузера к внешнему AI API.
Проксировать запрос вслепую недостаточно. Без авторизации, проверки размера и ограничения частоты ваш сервер станет общественным краном. Ключ спрятан, счёт всё равно ваш.
05
Если ключ уже был во frontend, удалять строку поздно
Считайте ключ раскрытым. Сначала отключите или перевыпустите его. Потом выпустите новую сборку без секрета, проверьте старые карты исходников и загруженные assets, посмотрите логи и временно опустите лимит. Удаление .env не отзывает значение, которое уже скачали посетители или сохранил CDN.
Если значение успело попасть в Git, одной новой правки тоже мало: оно остаётся в истории. GitHub умеет блокировать часть известных секретов при отправке, но сам предупреждает об ограничениях распознавания. Защита полезна. Планом хранения ключей она не является.
1. Отозвать
Старый ключ перестаёт оплачивать чужие запросы.
2. Перенести
Новый ключ хранится только на сервере или в его secret storage.
3. Ограничить
Отдельный проектный лимит сдерживает ущерб следующей ошибки.
4. Проверить
В production bundle ищутся реальные префиксы и контрольная канарейка.
Один продукт — один ключ и один предел ущерба
В TokenTool создайте отдельный проект и ключ для нового приложения. Поставьте дневной лимит, чтобы эксперимент не мог съесть общий баланс. Запросы отправляйте с вашего сервера, а в браузер возвращайте только нужный результат.
Если ключ придётся отключить, остальные продукты продолжат работать. По логам будет видно расход именно этого приложения, без попытки угадать виновника по одной общей строке списаний.
Что проверялось
- Vite: переменные окружения и предупреждение о клиентских секретах
- Next.js: как NEXT_PUBLIC-переменные встраиваются в браузерный JavaScript
- GitHub: возможности и границы push protection
Локальный опыт проведён 15 сентября 2026 года с заведомо недействительной канарейкой. Никакие пользовательские ключи, запросы или production-данные не использовались.