VIBEOPS CLUB
RU

VibeOps Club / Гайды

API-ключи и секреты в приложениях из Cursor, Lovable и Claude Code: где им место и как проверить

Агент кладёт ключ туда, где код заработает быстрее всего, и в приложении без собственного бэкенда это часто браузер. Разберём, какие ключи можно показывать всем, какие должны оставаться на сервере и как за 15 минут проверить на утечки уже работающее приложение.

В октябре 2025 года Escape проанализировали более 5 600 публичных приложений, сделанных вайбкодингом на Lovable, Base44, Create.xyz, Bolt.new и Vibe Studio и нашли свыше 400 открытых секретов, а также 2 000+ уязвимостей и 175 случаев утечки персональных данных (разбор кейса).

Публичные и секретные ключи

Большинство сервисов выдают два вида ключей. Публичный (publishable) ключ идентифицирует ваш проект и рассчитан на то, что его встроят в веб-страницу или мобильное приложение. Секретный ключ действует от вашего имени с полными или расширенными правами и не должен покидать сервер.

Можно отдавать в браузер

  • Publishable-ключ Supabase (sb_publishable_...) и старый anon-ключ. В браузере они безопасны только при включённом Row Level Security на каждой таблице и правильно написанных политиках.
  • Publishable-ключ Stripe (pk_live_..., pk_test_...). С ним можно создать токен или платёжный метод, но нельзя провести списание или прочитать данные аккаунта.

Только на сервере

  • Secret-ключ Supabase (sb_secret_...) и старый service_role. Оба работают от Postgres-роли service_role, которая полностью обходит RLS. Если новый secret-ключ прислать из браузера, Supabase ответит HTTP 401. Это страхует от части ошибок, но встраивать любой из этих ключей во фронтенд всё равно нельзя.
  • Секретные и ограниченные ключи Stripe (sk_..., rk_...).
  • Ключи OpenAI и Anthropic (sk-..., sk-ant-...). Публичных вариантов у этих провайдеров нет. С любым ключом к ИИ-API кто угодно может отправлять запросы за ваш счёт.
  • Строки подключения к базе, секреты подписи вебхуков, токены хостинга и инфраструктуры (Railway, Vercel, Cloudflare, AWS и других).

Публичный ключ безопасен, только если правила доступа за ним настроены правильно. У Moltbook ключ Supabase лежал в клиентском JavaScript, а RLS был выключен. В январе 2026 года исследователи Wiz обнаружили, что база читается без всякой авторизации: около 1,5 млн API-ключей, ~35 000 email и 4 060 личных сообщений. Команда закрыла доступ в течение нескольких часов после сообщения (кейс Moltbook). Если приложение обращается к Supabase напрямую из браузера, политики RLS фактически и есть ваш бэкенд. Как их проверить, описано в чек-листе продакшн-деплоя.

Всё, что получил браузер, публично

Любое значение из JavaScript-бандла может прочитать каждый, кто открыл сайт, и минификация строки не прячет. Его видно в DevTools на вкладке Sources, в скачанных .js-файлах и на вкладке Network, если значение уходит в заголовке запроса. Если при деплое публикуются ещё и source maps (файлы .map), доступен исходный код целиком и в читаемом виде.

Во фреймворках здесь легко ошибиться: попадёт ли переменная окружения в клиентский бандл при сборке, решает префикс в её имени.

  • NEXT_PUBLIC_ в Next.js. Значение вписывается в JavaScript, который уходит в браузер.
  • VITE_ в Vite, на котором сделаны многие проекты из Lovable и Bolt. Документация Vite прямо предупреждает, что API-ключам в таких переменных не место.
  • EXPO_PUBLIC_ в Expo. Значение лежит открытым текстом в собранном мобильном приложении.

Попасть в эту ловушку можно случайно. Клиентский код читает OPENAI_API_KEY, получает undefined, переменную переименовывают в NEXT_PUBLIC_OPENAI_API_KEY. Ошибка исчезает, а ключ становится публичным. Любой секрет с одним из этих префиксов считайте утёкшим.

Где должны жить секретные ключи

Если функции нужен секретный ключ, вызов должен выполняться на сервере, который вы контролируете: в API-роуте, serverless-функции, Supabase Edge Function или Cloudflare Worker. Браузер обращается к вашему эндпоинту, эндпоинт проверяет, кто спрашивает, и только потом использует ключ.

// app/api/summarize/route.js (выполняется на сервере)
export async function POST(req) {
  const user = await getSessionUser(req);      // ваша авторизация
  if (!user) return new Response('Unauthorized', { status: 401 });
  if (await overLimit(user.id)) return new Response('Too many requests', { status: 429 });
  if (!user.hasActiveSubscription) return new Response('Payment required', { status: 402 });

  const { text } = await req.json();
  const result = await summarize(text, process.env.OPENAI_API_KEY); // без NEXT_PUBLIC_
  return Response.json(result);
}

Перенести ключ на сервер недостаточно, нужны ещё проверки вокруг него: авторизация, лимит запросов на пользователя и проверка подписки на сервере. 15 марта 2025 года основатель EnrichLead написал, что его SaaS собран в Cursor и в нём «zero hand written code». Через два дня он сообщил, что его атакуют: лимиты по API-ключам выбраны до конца, подписку обходят, в базе появляется мусор. 20 марта он объявил о закрытии. Комментаторы связали это с тем, что ключи и проверка подписки жили на клиенте, а серверной авторизации и лимитов не было (кейс EnrichLead).

Сами значения хранятся в переменных окружения в панели или хранилище секретов вашей платформы (такое есть у Vercel, Netlify, Cloudflare, Railway и Supabase), а для локальной разработки в файле .env. Конфиги в репозитории и константы в коде для этого не подходят.

Репозиторий: .gitignore и история коммитов

Добавьте .env и .env.* в .gitignore до первого коммита, добавьте туда же исключение !.env.example и закоммитьте .env.example с именами переменных без значений, чтобы агент и коллеги понимали, что нужно заполнить. Если .env уже в репозитории, команда git rm --cached .env перестанет его отслеживать, но ключ останется во всех прошлых коммитах. Удаление файла убирает его только из текущей версии. Любой, кто клонирует репозиторий или его публичный форк, получает всю историю.

Секреты за пределами .env опасны не меньше. В апреле 2026 года в PocketOS ИИ-агент нашёл токен Railway CLI в файле, не связанном с задачей. Токен создавали для управления доменами, но он разрешал любые операции, и агент за 9 секунд удалил им продакшн-базу вместе с бэкапами на уровне томов. Данные восстановили примерно через час с помощью Railway (кейс PocketOS). Агент может прочитать всё в рабочей папке, поэтому инфраструктурным токенам там не место.

Сканирование секретов

Сканеры ловят ключи до того, как они попадут в удалённый репозиторий, или вскоре после этого. Подключите хотя бы один:

  • GitHub push protection. Для личного аккаунта включена по умолчанию и блокирует пуш с распознанными секретами в публичные репозитории. Защита на уровне приватного репозитория требует GitHub Secret Protection, и включать её должен администратор.
  • gitleaks как pre-commit хук: коммит с ключом упадёт ещё локально.
  • trufflehog, который вдобавок умеет проверять, действует ли найденный ключ.

Как проверить приложение за 15 минут

  1. Поищите ключи в собранном бандле. Соберите продакшн-версию и пройдитесь по папке сборки (dist у Vite, .next/static у Next.js):

    grep -rnoE '\bsk-[A-Za-z0-9_-]{20,}|sk_live_[A-Za-z0-9]+|sb_secret_[A-Za-z0-9_-]+|service_role|eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+' dist/

    Любое совпадение с sk-, sk_live_, sb_secret_ или service_role означает утечку. Строки, начинающиеся с eyJ, это JWT, и старый anon-ключ Supabase тоже JWT. Раскодируйте его локально, не вставляя на сторонние сайты:

    node -e "console.log(Buffer.from(process.argv[1].split('.')[1],'base64url').toString())" "ВСТАВЬТЕ_ТОКЕН"

    "role":"anon" в порядке вещей. "role":"service_role" значит, что админский ключ лежит во фронтенде.

  2. Посмотрите вкладку Network на живом сайте. Откройте DevTools, пройдитесь по основным функциям и посмотрите, куда уходят запросы. Если браузер сам ходит на api.openai.com, api.anthropic.com или api.stripe.com с заголовком Authorization, секретный ключ находится на клиенте. Заодно проверьте на вкладке Sources, загружаются ли файлы .map.
  3. Просканируйте репозиторий вместе с историей. Установите gitleaks (brew install gitleaks или бинарник из релизов) и запустите из корня репозитория:

    gitleaks git -v --redact .     # все коммиты, а не только текущие файлы
    gitleaks dir -v --redact dist  # собранный бандл

    Альтернатива, которая заодно проверяет найденные ключи у провайдера: trufflehog git file://. --results=verified,unknown.

  4. Убедитесь, что .env игнорируется и никогда не попадал в git.

    git check-ignore -v .env        # выведет правило из .gitignore или ничего
    git ls-files | grep -i '\.env'   # в списке должен быть только .env.example
  5. Проверьте публичный ключ от имени анонима. Возьмите ключ Supabase из бандла и запросите таблицу с пользовательскими данными:

    curl "https://YOUR_PROJECT.supabase.co/rest/v1/profiles?select=*" -H "apikey: YOUR_PUBLISHABLE_KEY"

    Ожидаемый ответ: пустой массив или ошибка доступа. Если пришли настоящие строки, RLS не включён или политика слишком широкая.

Если ключ утёк

  1. Сначала перевыпустите ключ. Отзовите его в панели провайдера, создайте новый, обновите переменную окружения и передеплойте. GitHub в своей документации ставит этот шаг раньше чистки истории: отозванный ключ бесполезен тому, кто его скопировал.
  2. Проверьте расход и счета. Посмотрите логи использования и биллинг провайдера за весь период, пока ключ был открыт. Для ключа от базы выясните, что успели прочитать или изменить и нужно ли уведомлять пользователей.
  3. Почистите историю, если репозиторий публичный или общий, с помощью git filter-repo. В форках, клонах и кэшах старые коммиты могут остаться, поэтому ротация идёт первой.
  4. Устраните причину. Добавьте сканер или серверный эндпоинт, который поймал бы эту утечку, чтобы она не вернулась со следующей сгенерированной фичей.

Как ограничить ущерб заранее

Исходите из того, что рано или поздно какой-то ключ утечёт, и заранее решите, во сколько это может обойтись. Заведите отдельные ключи для разработки, стейджинга и продакшена, чтобы ключ с ноутбука или из превью-деплоя не дотягивался до боевых данных. Выдавайте каждому ключу минимальные права, которые позволяет провайдер: ограниченные ключи Stripe с доступом только к нужным ресурсам, облачные токены на один проект, доступ только на чтение там, где это возможно. Поставьте месячные лимиты расходов или бюджетные алерты в консолях OpenAI и Anthropic и проверьте, жёсткий это лимит или только уведомление. Ограничьте частоту запросов на всех эндпоинтах, которые вызывают платные API. Подробнее об этом в разделе чек-листа про расходы.

Чек-лист

  • В браузер попадают только публичные ключи. Ни у одного секрета нет префикса NEXT_PUBLIC_, VITE_ или EXPO_PUBLIC_.
  • RLS включён на каждой таблице Supabase, анонимный запрос с публичным ключом не возвращает приватных данных.
  • Вызовы ИИ-API, Stripe и админские операции с базой идут через серверный эндпоинт с проверкой пользователя и лимитом запросов.
  • Секреты хранятся в переменных окружения хостинга. .env есть в .gitignore и ни разу не был закоммичен.
  • gitleaks или trufflehog ничего не находят во всей истории git и в собранном бандле.
  • Инфраструктурных токенов нет ни в репозитории, ни в файлах, которые может прочитать агент.
  • Для каждого окружения свои ключи, и у каждого только нужные права.
  • У ИИ-провайдеров стоят лимиты расходов или алерты, и вы можете перевыпустить любой ключ меньше чем за десять минут.

Задача для агента

С таким аудитом агенты справляются хорошо, если у задачи есть чёткие критерии приёмки. Вставьте текст ниже в Cursor, Claude Code или Lovable, а дифф проверьте сами:

Проверь проект на секреты, доступные клиенту.

1. Перечисли все переменные окружения, которые читает код, с файлом и строкой, и отметь для каждой: публичная или секретная.
2. Каждый секрет, который используется в клиентском коде или имеет префикс NEXT_PUBLIC_, VITE_ или EXPO_PUBLIC_, перенеси в серверный эндпоинт с проверкой залогиненного пользователя и лимитом запросов на пользователя.
3. Проверь, что .env* есть в .gitignore с исключением для .env.example, и создай .env.example только с именами переменных.
4. Сам ключи не перевыпускай и не удаляй, продакшен не трогай.

Критерии приёмки: после npm run build поиск по папке сборки на sk-, sk_live_, sb_secret_ и service_role ничего не находит; браузер не делает прямых запросов к api.openai.com, api.anthropic.com и api.stripe.com; gitleaks git -v --redact . не находит утечек, а если находит, перечисли все находки, чтобы я перевыпустил эти ключи. Покажи дифф и список ключей, которые мне нужно перевыпустить.

Секреты открывают чек-лист продакшн-деплоя для вайбкодеров. Чем заканчивается пропуск этих шагов, видно по статье 8 реальных инцидентов вайбкодинга.

VibeOps Club

Проверьте секреты в своём проекте вместе с нами

Секреты и управление доступом входят в программу 2-недельной когорты отдельным модулем. Вы проводите этот аудит на своём сервисе, ставите задачи агенту, а результат мы разбираем вместе. 10 мест, 1:1 консультации, capstone на вашем проекте.

Вступить в клуб