API‑ключи в сборках фронтенда: как найти и исправить утечки
API‑ключи в сборках фронтенда могут раскрыть ваши данные. Узнайте, как найти встроенные секреты, перенести их на сервер и безопасно провести ротацию.

Что значит — утекли секреты через сборку фронтенда
Сборка фронтенда — это набор файлов, которые ваши пользователи скачивают, чтобы запустить приложение в браузере. Обычно туда входят собранный JavaScript (часто один большой файл), CSS, изображения и другие статические ресурсы. Инструменты сборки берут исходники и пакуют их так, чтобы браузеру было быстро их загружать.
Утечка происходит, когда что‑то, что вы хотели держать в секрете, попадает в эти скачиваемые файлы. Если оно ушло в браузер, любой может увидеть это через "View Source", DevTools или скачав бандл и поискав по нему.
Секреты — это всё, что позволяет кому‑то выдать себя за ваше приложение или получить доступ к защищённым системам. Частые примеры: API‑ключи и сервисные токены (Stripe, OpenAI, провайдеры карт, сервисы почты), строки подключения к БД, секреты подписи JWT, приватные ключи шифрования, приватные URL вебхуков, админ‑эндпойнты и внутренние URL, обходящие авторизацию.
Поэтому API‑ключи в сборке фронтенда следует считать публичными. Минификация и обфускация не спасут. Если браузеру нужно получить значение — злоумышленник тоже сможет.
Последствия обычно просты: кто‑то скопирует ваш ключ и устроит вам счёт, исчерпает лимиты, чтобы реальные пользователи получили ошибки, или будет спамить провайдеров от вашего имени. Если секрет открывает доступ к данным, это перерастает в утечку (записи клиентов, файлы или админ‑действия). Убытки часто начинаются с малого и быстро растут, особенно если ключ обладает широкими правами.
Частые пути попадания секретов в клиентский код
Большинство утечек — это не «взломы». Они происходят из обычных настроек сборки и конфигурации, где секреты по ошибке обращаются как публичные параметры.
Инъекция во время сборки превращает .env в код, который шлётся клиенту
Инструменты вроде Vite, Next.js и Create React App могут подставлять переменные окружения во время сборки. Если секрет используется в клиентском коде, бандлер часто заменит его на обычную строку. Значение, которое безопасно лежало в локальном .env, может оказаться в продакшен‑JavaScript.
Типичная ошибка — думать «ну это в env, значит безопасно». Оно безопасно только если используется на сервере и никогда не попадает в клиентские бандлы.
Префиксы «для публичного доступа» — это для публичных значений
Фреймворки используют префиксы, чтобы пометить переменные, которые могут быть видны в браузере (например: VITE_, NEXT_PUBLIC, REACT_APP). Они подходят для несекретных настроек: флагов функций или публичного идентификатора аналитики.
Но они не для всего, что может тратить деньги, получать приватные данные или давать повышенные права: ключи третьих сторон, создающие списания, админ‑токены, ключи подписи, URL базы данных, креденшелы сервисов и секреты вебхуков.
Если вы ставите секрет за публичным префиксом — вы прямо просите систему сборки опубликовать его.
Хардкодированные строки и «полезные» файлы конфигурации
Секреты также просачиваются через константы, объекты конфигурации и JSON‑файлы (например, config.ts, firebaseConfig или settings.json). При ревью они выглядят как безобидная настройка приложения.
Это особенно часто встречается в AI‑сгенерированных прототипах: инструменты часто вставляют рабочий пример с ключом inline, чтобы демо «просто работало». Если такой код попадает в продакшен — ключ становится публичным.
Source maps упрощают поиск
Source maps сами по себе не создают утечку, но делают её проще найти. Если карты доступны публично, злоумышленник может искать читабельный исходник для токенов, эндпойнтов и похожих строк, вместо сканирования минифицированных бандлов.
Если вы подозреваете, что секрет был скомпрометирован — принимайте, что он уже скомпрометирован. Планируйте перенос на сервер и ротацию.
Быстрые способы найти открытые env‑переменные и API‑ключи
Часто подтвердить утечку можно за считанные минуты без изменения кода. Начните с того, что уже есть в браузере.
-
Просмотрите сетевые запросы в DevTools. Откройте вкладку Network и пройдите потоки, которые вызывают сторонние сервисы (логин, оплата, поиск, загрузка файлов). Откройте запрос и проверьте Headers и Preview/Response. Утечки часто видны в query‑параметрах (например
?api_key=), в заголовкеAuthorization: Bearer ...или в JSON‑теле, где есть токен. -
Просканируйте то, что вы действительно шлёте. В папке итоговой сборки (часто
dist/илиbuild/) выполните текстовый поиск по.jsфайлам: паттерны вродеsk-,api_key,apikey,secret,token,Bearer,BEGIN PRIVATE KEY, а также имена вендоров и базовые URL (Stripe, OpenAI, Twilio, Supabase и т. п.). -
Откройте скомпилированный бандл и просканируйте на подозрительные строки. Ищите хардкодированные креденшелы, полные эндпойнты или конфиг‑блоки с ключами. Красный флаг — рантайм‑конфиг на
window(напримерwindow.__CONFIG__илиwindow.env) с чем‑то более чувствительным, чем публичные ID. -
Проверьте, доступны ли в продакшене source maps. Публичные
*.mapфайлы раскрывают исходный код и структуру конфигурации, что ускоряет повторное использование.
Практический тест: если вы можете скопировать токен из DevTools и воспроизвести запрос из другого профиля браузера или скрипта — это не безопасное клиентское значение.
Спланируйте исправления прежде, чем менять код
Как только вы подтвердили утечку, не начинайте без плана массово удалять переменные или ротации. Небольшое планирование предотвратит простои, сломанные логины и запутанные 401‑ошибки.
Начните с списка всех внешних систем, с которыми общается ваше приложение, и где сейчас хранятся креденшелы: платёжные системы, почта, хранилище файлов, аналитика, API для AI и прочее. Учитывайте staging, preview и локальную разработку. Утечки часто происходят в preview‑сборках, потому что их считают одноразовыми.
Далее разделите, что может вызываться из браузера, а что должно оставаться только на сервере. Всё, что может тратить деньги, читать приватные данные, писать в базу или обходить лимиты — не должно вызываться напрямую с клиента. Если сервис даёт publishable‑ключ для браузеров — относитесь к нему иначе, чем к секретному ключу.
Создайте простой инвентарь, которым команда сможет поделиться:
- Имя ключа как в коде (например
STRIPE_SECRET_KEY) - Сервис и аккаунт/проект, к которому он принадлежит
- Окружения (dev, staging, prod)
- Где встречается (репо, CI, настройки хостинга, собранные файлы)
- Уровень риска (низкий для публичных ID, высокий для секретов)
Затем решите, что ротировать немедленно, а что после изменений в коде. Ротуйте прямо сейчас, если утёкший ключ даёт права на запись, широкие scope‑права или может слить кредиты. Отложите ротацию, если она сломает продакшен до того, как у вас появится серверная замена.
Назначьте короткое окно блокировки (даже 30–60 минут), когда никто не мерджит, не деплоит и не меняет переменные окружения хостинга, пока вы координируете действия. Назначьте владельца по каждому сервису и согласуйте порядок: сперва выкладывайте серверные изменения, потом делайте ротацию, затем деплой и верификация.
Пошагово: аудит, удаление и повторное тестирование сборки
Обращайтесь к этому как к инциденту: найдите, что утекло, остановите утечку, затем докажите, что всё исправлено.
1) Аудит того, что есть (репозиторий и билд)
Ищите как в исходниках, так и в собранных артефактах (итоговые файлы, которые скачивают пользователи). Не ограничивайтесь репозиторием: секреты могут скрываться в минифицированных бандлах.
Простой рабочий процесс:
- Сканируйте на паттерны похожие на секреты: длинные токены, префиксы провайдеров (например
sk_,xoxb-) и строкиBearer. - Проверьте использование env:
process.env,import.meta.envи любые публичные env‑переменные фреймворка. - Ищите в собранных JS очевидные строки: ваш API‑домен, префиксы ключей или JSON‑блоки с креденшелами.
- Подтвердите, где это встречается: в исходниках, в конфиге или только после шага сборки.
- Запишите каждое совпадение и где оно найдено до внесения изменений.
2) Подтвердите, что действительно чувствительно
Не каждое похожее на ключ значение опасно. Некоторые сервисы дают публикуемые ключи, предназначенные для браузеров, в то время как другие дают полный доступ.
Задайте два вопроса:
- Что может сделать этот креденшел?
- Может ли злоумышленник использовать его со своей машины?
Если ответы «да» и «да», считайте его чувствительным.
3) Удалите из клиента, замените вызовом на сервере
Удалите секрет из клиентского кода и из любых build‑time env, которые попадают в браузер. Замените прямой вызов клиент→провайдер на серверный эндпойнт, который использует секрет на сервере. Фронтенд должен вызывать ваш endpoint, а не провайдера напрямую.
4) Повторно протестируйте с новым ключом (сначала не в продакшене)
Создайте новый ключ, протестируйте end‑to‑end в стейджинге или preview, и только потом переключайте продакшен. Пересоберите и пересканируйте вывод, чтобы подтвердить, что секрет действительно ушёл.
Перенос секретов на сервер без поломки приложения
Самый надёжный фикс — не пытаться «лучше спрятать в JavaScript», а вообще перестать посылать секрет в браузер.
Базовый шаблон: прокси на сервере
Пусть клиент вызывает ваш бэкенд‑эндпойнт (или serverless‑функцию). Этот эндпойнт общается с третьей стороной и подставляет секрет на сервере. Браузер никогда не видит ключ.
Простой подход, сохраняющий поведение UI:
- Замените прямой вызов провайдера в фронтенде на вызов своего эндпойнта.
- На сервере читайте секрет из серверных env‑переменных.
- Возвращайте только те данные, которые нужны интерфейсу, а не полный upstream‑ответ.
Не создавайте открытый прокси. Валидируйте входные данные на сервере: разрешайте только нужные вам маршруты, проверяйте обязательные поля и блокируйте неожиданные URL или заголовки. Если ваш endpoint принимает «target URL» от клиента, вы воссоздали утечку в новой форме.
Предпочитайте короткоживущие токены
Если провайдер поддерживает, генерируйте короткоживущие токены на сервере и отправляйте в клиент только их. Токены с истечением через минуты безопаснее, чем долгоживущие ключи, встроенные в бандл.
Сочетайте это с базовыми серверными контролями: rate‑лимиты и логирование запросов, чтобы злоупотребления быстро заметны (всплески трафика, повторяющиеся ошибки, необычные полезные нагрузки).
Сохраняйте в клиенте только публичные идентификаторы (например публичный ID проекта) и не‑секретные флаги (например useSandbox: true). Всё, что даёт доступ или может создавать списания, — на сервер.
Безопасная ротация ключей с минимальным простоем
Ротация — это не просто «создать новый и вставить». Если отозвать старый ключ слишком рано, реальные пользователи получат ошибки. Если оставить старый активным слишком долго — утечка продолжит эксплуатироваться. Стремитесь к короткому контролируемому перекрытию.
Самый безопасный способ — двухфазная ротация: сначала добавьте новый ключ, подтвердите его использование, затем отзывайте старый.
Низко‑рисковая последовательность ротации
Делайте это, когда можете наблюдать логи:
- Создайте новый ключ в панели провайдера. Дайте понятное имя (например
prod-2026-01-rotation). - Добавьте новый ключ в хранилище секретов на сервере (не в сборку фронтенда). Старый ключ пока оставьте активным.
- Задеплойте версию, использующую новый ключ.
- Подтвердите использование по логам провайдера и вашим логам приложения.
- После короткого окна перекрытия (обычно 15–60 минут, дольше при медленных деплойах или долгоживущих задачах) отзовите старый ключ.
Поставьте напоминание отозвать старый ключ — команды часто забывают в разгар инцидента.
Не забудьте «скрытые» вызывающие стороны
Перед отзывом убедитесь, что вы обновили:
- Фоновые задачи и cron‑джобы
- Вебхуки и интеграции третьих сторон, которые вызывают ваш API
- Мобильные приложения (они обновляются дольше и могут отставать)
- Стейджинг‑окружения, которые случайно используют прод‑ключи
- Локальные настройки разработчиков, которые всё ещё держат старый ключ
После деплоя следите за уровнем ошибок и неудач авторизации. Если что‑то ломается — быстро откатывайте или увеличивайте окно перекрытия, пока не найдёте пропущенного вызывающего.
Распространённые ошибки, которые поддерживают утечку
Если вы видите значение в браузере (View Source, DevTools, сетевые запросы, бандл JS) — увидит и злоумышленник.
- Минификация и обфускация не защищают секреты. Ключ, вшитый в бандл, остаётся ключом, даже если его разнесли по переменным.
- Долгоживущие токены в
localStorageилиsessionStorageлегко украсть через XSS. Предпочитайте серверные сессии (HTTP‑only cookies) и короткоживущие креденшелы. - Отладочные эндпойнты и админ‑маршруты остаются открытыми. Всё, что обходило авторизацию или дампило данные в тестировании, может превратиться в путь к продакшен‑данным.
- Клиент‑логирование ошибок может утекать секреты. Не логируйте в браузере полные заголовки запросов, токены или дампы конфигурации.
- CORS не является пограничной мерой безопасности. Он лишь ограничивает, какие браузеры могут читать ответы; он не останавливает прямые вызовы от скриптов, серверов или инструментов вроде curl.
Быстрый чек‑лист перед деплоем
Используйте это прямо перед релизом.
5 проверок, предотвращающих большинство утечек
- Подтвердите чистоту клиентского бандла: просканируйте собранный вывод (не только исходники) на ключевые паттерны (например
sk_,AIza,Bearer,-----BEGINили имя вашего вендора). Также убедитесь, что вы не используете публичные префиксы env (какNEXT_PUBLIC_илиVITE_) для всего, что должно быть приватным. - Считайте source maps чувствительными: если вы храните их в продакшене, ограничьте доступ. Если они не нужны — отключите их для production‑сборок.
- Перенесите секреты за серверную границу: любой вызов, который требует секрета, должен идти через ваш сервер (или serverless‑функцию). Браузер должен отправлять намерение пользователя и получать безопасные результаты.
- Закройте серверные эндпойнты: валидируйте вход, проверяйте аутентификацию и применяйте rate‑лимиты.
- Ротуйте ключи и следите за злоупотреблениями: создавайте новые ключи сначала, деплойте изменение, потом отзывайте старые. Мониторьте резкий рост трафика или расходов.
Простой пример: если ваш фронтенд вызывает ключ напрямую для карт или AI‑провайдера, этот ключ быстро будет скопирован и переиспользован. Вместо этого браузер вызывает ваш backend‑эндпойнт, а backend звонит провайдеру с секретом. В браузере нет ничего ценного для кражи.
Пример: AI‑сгенерированный прототип, где ушёл реальный API‑ключ
Основатель быстро сделал MVP в Lovable и задеплоил в тот же день. Приложение обращалось к платному API (LLM, карты, почта или аналитика), а ключ был в переменной окружения. Инструмент сборки подставил это значение в бандл, и ключ оказался в скомпилированном JavaScript.
Любопытный пользователь открыл DevTools, нашёл «key» или имя провайдера в Sources и скопировал значение из минифицированного файла. Через несколько часов использование взлетело, расходы выросли, а в дашборде провайдера виден поток трафика, не совпадающий с реальными пользователями.
Фикс прост, но порядок действий важен:
- Перенесите вызов API за сервер (или serverless‑функцию), чтобы браузер никогда не видел секрет.
- Добавьте защиту на этот маршрут: проверки аутентификации, rate‑лимиты и валидацию входа.
- Ротуйте открытый ключ и отзовите старый после короткого окна перекрытия.
- Просмотрите логи провайдера на подозрительные IP, неизвестные эндпойнты и трафик вне обычных часов.
- Включите оповещения и лимиты расходов, чтобы получать предупреждения до того, как счёт вырастет.
После изменений UI по‑прежнему вызывает ту же фичу, но делает запрос к вашему бекенду, а не к провайдеру напрямую. Бэкенд читает ключ из серверных env и общается с третьей стороной от имени пользователя. В браузере нет ничего ценного для кражи.
Следующие шаги, если кодовая база запутана или AI‑сгенерирована
Если приложение было сгенерировано инструментами вроде v0, Cursor, Replit, Bolt или Lovable, предполагайте, что секреты могли быть скопированы в несколько мест: .env, вспомогательные конфиги, временные логи и сам клиентский бандл. Поэтому быстрые фиксы часто не помогают: вы удаляете одну копию, но другая всё ещё идёт в билд.
Сфокусируйтесь на всех путях, по которым значение может пройти, а не только на очевидном: инъекция во время сборки, включённые конфиги в бандле, serverless‑функции и потоки аутентификации, которые пропускают токены через браузер.
Практический подход при ненадёжном репозитории:
- Определите самые важные секреты (платёжные, почта, база данных, админ‑API).
- Ищите по репозиторию паттерны ключей и имена env, затем проверьте и собранные артефакты.
- Проследите, как делаются запросы: напрямую из браузера к провайдеру или через ваш сервер.
- Решите, где на сервере будет жить каждый секрет (API‑маршрут, бэкенд‑служба, прокси).
- Спланируйте ротацию ключей после изменения кода, чтобы избежать неожиданных простоев.
Если вы унаследовали сломанный AI‑сгенерированный прототип и нужен быстрый безопасный проход, FixMyMess (fixmymess.ai) выполняет диагностику кода и исправления для проблем вроде встроенных секретов, сломанной аутентификации и небезопасных паттернов запросов; они предлагают бесплатный аудит кода, чтобы замапить, что открыто, прежде чем вы начнёте ротацию ключей.
Часто задаваемые вопросы
Что считается «секретом» в сборке фронтенда?
Если браузеру нужен этот ключ для работы — он фактически публичный. Всё, что может тратить деньги, читать приватные данные, записывать в базу или подписывать/проверять токены, следует считать секретом и держать на сервере.
Защищают ли минификация или обфускация API‑ключи в JavaScript‑бандлах?
Нет. Минификация меняет только внешний вид кода, но не его содержимое. Любой может скачать бандл и искать строки, поэтому ключ, встроенный в JavaScript, остаётся видимым.
Безопасно ли использовать переменные окружения с префиксами NEXT_PUBLIC_, VITE_, REACT_APP_ для секретов?
Эти префиксы явно предназначены для значений, которые могут быть показаны в браузере. Используйте их только для не‑секретных настроек, например публичных идентификаторов или флагов UI, и никогда — для секретных ключей, токенов сервисов или URL баз данных.
Как быстро понять, что ключ утекает, без изменения кода?
Откройте сайт, включите DevTools и проверьте сетевые запросы на наличие query‑параметров, заголовков или тел запросов с токенами. Затем просканируйте собранные файлы (build/dist) на паттерны вроде «Bearer», «api_key», названия провайдеров или узнаваемые префиксы ключей.
Почему публичные source maps усугубляют утечки секретов?
Источник утечки остаётся, но source map делает поиск и повторное использование проще: он раскрывает читаемый исходник и имена переменных. Если вы храните карты в продакшене — ограничьте к ним доступ или отключите их для production‑сборок.
Что нужно сделать в первую очередь после обнаружения открытого ключа?
Считайте, что он скомпрометирован, и остановите утечку, прежде чем долго обсуждать. Зафиксируйте, где вы нашли ключ, определите, что он умеет делать, и спланируйте перенос на сервер и безопасную ротацию.
Как безопасно вынести секрет из фронтенда, не нарушив функциональность?
Замените прямой вызов поставщика из браузера на бэкенд‑эндпойнт (или serverless‑функцию), который делает запрос к провайдеру, подставляя секрет уже на сервере. Фронтенд должен передавать намерение пользователя и получать только безопасные результаты, а не нести учётные данные провайдера.
Можно ли хранить токены в localStorage или sessionStorage?
Это рискованно: при любой XSS‑уязвимости такие токены легко украсть и повторить запрос. Предпочитайте серверные сессии и короткоживущие креденшелы, и по возможности не храните долгоживущие секреты в localStorage или sessionStorage.
Как сменить утёкший ключ без простоя?
Используйте двухфазную ротацию: сначала добавьте новый ключ и подтвердите его использование, затем отзывайте старый после короткого окна перекрытия. Если отозвать старый слишком рано — вы сломаете пользователей; если оставить его долго — злоумышленники продолжат злоупотреблять.
Часто ли AI‑генерированные прототипы протекают, и может ли FixMyMess помочь?
Да. AI‑генерированные прототипы часто вставляют рабочие ключи в код, копируют их в временные конфиги или используют env‑переменные в клиентском коде, из‑за чего ключи попадают в сборку. Если репозиторий вызывает недоверие, FixMyMess может провести бесплатный аудит кода, проследить все пути утечки и быстро силой безопасности исправить проблему — обычно в течение 48–72 часов, а в экстренных случаях план перестройки можно начать примерно за день.