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
Sources