← Vibe Coding — and How to Take It to Production
04 / 07 · 50 min

Security Traps of Vibed Apps

Exposed API keys in the bundle, Supabase without RLS, trusting the client, and IDOR: find and close the holes before someone else does.

Lesson content is currently available in Russian.

What you will learn

  • →Находить секреты в клиентском коде и переносить их на сервер.
  • →Включать Row Level Security в Supabase и писать базовые политики доступа.
  • →Проверять серверную валидацию и права владельца вместо доверия клиенту.

Открытые API-ключи: всё, что в браузере, — публично

Первый закон безопасности фронтенда: любой код и любая строка, попавшие в браузер, доступны каждому пользователю. Достаточно открыть DevTools, вкладку Sources или Network — и весь клиентский бандл читается как открытая книга. Сгенерированные приложения нарушают этот закон постоянно: агенту проще положить ключ рядом с местом использования.

В Next.js и Vite действует явное соглашение: переменные окружения с префиксом NEXT_PUBLIC_ или VITE_ встраиваются в клиентский бандл. Это место для публичных значений — адреса API, публичного ключа карт. Классическая ошибка вайб-кодинга:

NEXT_PUBLIC_OPENAI_API_KEY=sk-proj-...   ← ключ у каждого посетителя
OPENAI_API_KEY=sk-proj-...               ← ключ только на сервере

Первый вариант означает, что любой посетитель может скопировать ваш ключ и жечь ваши деньги на своих задачах. Такие ключи находят боты, сканирующие публичные сайты и репозитории, — обычно в течение часов.

Отдельная история — ключи Supabase. Ключ anon публичный по замыслу: он рассчитан на работу из браузера, но только в паре с политиками RLS (следующая секция). Ключ service_role — всесильный, он обходит все политики; его место — исключительно на сервере, и в клиентском коде его быть не может никогда.

Как искать утечки: глобальный поиск по проекту строк sk-, key, secret, password; проверка, что все секреты живут в .env, а .env внесён в .gitignore. Если секрет уже попал в git или в браузер — считайте его скомпрометированным: отзовите и выпустите новый. Удаление из кода не отменяет утечку.

Supabase без RLS: база нараспашку

Row Level Security (RLS) — механизм Postgres, который решает, какие строки таблицы видит и меняет конкретный пользователь. В связке с Supabase это главный замок вашей базы: anon-ключ публичен, значит, запрос к базе может отправить кто угодно — и единственное, что стоит между чужим любопытством и вашими данными, это политики RLS.

Таблица с выключенным RLS при публичном anon-ключе означает: любой человек, открыв DevTools, может прочитать, изменить или удалить все строки. Не «теоретически может» — для этого достаточно пары строк кода в консоли браузера. Именно поэтому Supabase показывает громкие предупреждения на таблицах без RLS, а истории «у стартапа утекла вся база пользователей» чаще всего начинаются со слов «мы забыли включить RLS».

Базовый набор для таблицы habits выглядит так:

alter table habits enable row level security;

create policy "read own habits" on habits
  for select using (auth.uid() = user_id);

create policy "insert own habits" on habits
  for insert with check (auth.uid() = user_id);

create policy "delete own habits" on habits
  for delete using (auth.uid() = user_id);

Читается так: включить замок; читать, добавлять и удалять можно только строки, где user_id совпадает с идентификатором вошедшего пользователя. using фильтрует существующие строки, with check проверяет новые.

Проверка обязательна и проста: зарегистрируйте второго тестового пользователя и убедитесь, что под ним не видно данных первого. Затем попробуйте выполнить select без входа. Пока оба теста не пройдены, приложение публиковать нельзя.

Доверие клиенту и IDOR

Вторая системная дыра vibed-приложений — сервер, который верит клиенту. Правило: клиентский код полностью подконтролен пользователю, поэтому любая проверка на клиенте — это удобство интерфейса, а не защита. Настоящая проверка живёт только на сервере.

Классика жанра — цена в запросе. Сгенерированный магазин нередко отправляет на сервер то, что посчитал в браузере:

// Антипаттерн: клиент диктует цену
await fetch('/api/checkout', {
  method: 'POST',
  body: JSON.stringify({ productId: 42, price: 0.01 }),
});

Пользователь меняет price в DevTools — и покупает за копейку. Правильно: клиент отправляет только productId и количество, а цену сервер берёт из своей базы.

Вторая классика — IDOR (Insecure Direct Object Reference), простыми словами «подставь чужой номер». Эндпоинт /api/orders/123 отдаёт заказ по номеру, но не проверяет, чей это заказ. Пользователь меняет 123 на 124 — и читает чужой заказ. Уязвимость обидно простая и стабильно входит в OWASP Top 10 (категория Broken Access Control — первое место списка 2021 года). Лечение одно: каждый серверный обработчик, работающий с объектом, обязан проверить, что объект принадлежит текущему авторизованному пользователю, — либо явным условием в запросе, либо политикой RLS.

Промпт для аудита своего приложения: «пройди по всем серверным эндпоинтам и вызовам базы; для каждого скажи, что произойдёт, если запрос отправит чужой или неавторизованный пользователь с произвольными параметрами». Ответ агента сверьте с кодом — как учил прошлый урок.

Чек-лист перед публикацией

Соберём урок в проверяемый чек-лист. Перед тем как дать ссылку на приложение кому-то, кроме себя, пройдите по шести пунктам.

Первое: секреты. Глобальный поиск по коду не находит ключей и паролей; все секреты в переменных окружения; файл .env в .gitignore; в клиентский бандл попадают только действительно публичные значения; всё, что успело утечь, — отозвано и перевыпущено.

Второе: RLS. На каждой таблице включён Row Level Security и написаны политики; тест вторым пользователем пройден; анонимный запрос не читает чужие данные.

Третье: серверная валидация. Цены, суммы и права берутся из базы, а не из тела запроса; каждый эндпоинт проверяет владельца объекта; входные данные проверяются по схеме (в JavaScript-мире стандарт — библиотека zod).

Четвёртое: границы ввода. Формы ограничивают длину и формат, загрузка файлов — размер и тип. Всё, что пользователь может отправить, однажды отправят с максимальной злобой.

Пятое: зависимости. Команда npm audit не показывает критических уязвимостей; случайные пакеты, которые агент поставил «на всякий случай», удалены.

Шестое: приватность ошибок. Тексты ошибок для пользователя не раскрывают внутренности: стек-трейсы и SQL-ошибки остаются в серверных логах, наружу уходит нейтральное сообщение.

Этот чек-лист не делает приложение неуязвимым — он закрывает дыры, через которые vibed-приложения ломают в первую очередь: не изощрёнными атаками, а простым любопытством с DevTools.

Practice

Аудит собственного прототипа

Прогоните свой прототип по чек-листу из шести пунктов: найдите секреты поиском по коду, включите RLS с политиками на все таблицы, протестируйте вторым пользователем, проверьте эндпоинты на IDOR промптом-аудитом.

Deliverable

Отчёт: список найденных дыр (до) и подтверждение закрытия (после) — вывод поиска секретов, SQL политик, результат теста вторым пользователем.

Checkpoint

Почему anon-ключ Supabase можно оставлять в клиенте, а service_role — никогда, и что должно быть настроено, чтобы anon-ключ был безопасен?

  • ✓ Объяснено, что весь клиентский код публичен, а service_role обходит все политики.
  • ✓ Названо условие безопасности anon-ключа: включённый RLS с политиками на каждой таблице.
  • ✓ Упомянут тест вторым пользователем как способ проверки политик.

Check yourself

Переменная NEXT_PUBLIC_OPENAI_API_KEY — почему это ошибка?

01Переменная NEXT_PUBLIC_OPENAI_API_KEY — почему это ошибка?

Что случится, если таблица Supabase останется без RLS при публичном anon-ключе?

02Что случится, если таблица Supabase останется без RLS при публичном anon-ключе?

Магазин отправляет цену товара с клиента на сервер. В чём проблема?

03Магазин отправляет цену товара с клиента на сервер. В чём проблема?

Что такое IDOR?

04Что такое IDOR?

API-ключ засветился в git-истории. Достаточно ли удалить его из кода?

05API-ключ засветился в git-истории. Достаточно ли удалить его из кода?

Sources

Security Traps of Vibed Apps · Vibe Coding — and How to Take It to Production · VORCL