EN Прислать проект
Все статьи

Row Level Security в Supabase: как на самом деле закрыть таблицы

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

· 4 минБезопасностьКод

В Supabase браузер ходит в базу почти напрямую. Между ним и твоими таблицами нет бэкенда, который проверил бы права, — есть только Row Level Security. Поэтому RLS здесь не дополнительная защита, а единственная. Если политика написана неправильно, никакой другой слой ошибку не поймает.

Хорошая новость: правила простые. Плохая: ошибиться в них легко, а приложение при этом продолжает работать как ни в чём не бывало.

Как это устроено

RLS включается на таблицу. Пока он выключен, таблицу видит любой, у кого есть публичный ключ, то есть весь интернет. Когда включён и политик нет, не видит никто, кроме сервисной роли. Политики открывают доступ обратно, строка за строкой.

У политики две части. using решает, какие существующие строки можно видеть, менять и удалять. with check решает, какие строки можно записать — при вставке и после обновления. Политики одной команды складываются через «или»: если одна разрешает всё, остальные уже не важны.

alter table notes enable row level security;

create policy "notes: read own" on notes
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "notes: insert own" on notes
  for insert to authenticated
  with check ((select auth.uid()) = user_id);

Обёртка (select auth.uid()) — не украшение: так Postgres вычисляет идентификатор один раз на запрос, а не на каждую строку. На больших таблицах разница заметна. И поставь индекс на user_id — политика по сути добавляет условие в каждый запрос.

Типичные ошибки

То, что мы находим чаще всего:

  • RLS включён не везде. Таблицу добавили позже, через миграцию или руками в редакторе, и про неё забыли. Список таблиц без RLS даёт select tablename from pg_tables where schemaname = 'public' and not rowsecurity;.
  • «Вошёл — значит можно». Политика using (auth.role() = 'authenticated') пускает к чужим данным любого зарегистрированного. Регистрация бесплатная, так что это почти то же, что не закрывать вовсе.
  • Вставка без with check. Читать чужое нельзя, а записать строку с чужим user_id — можно. Так появляются заказы и сообщения «от имени» другого человека.
  • Роль из user_metadata. Эти поля человек меняет сам через клиентскую библиотеку. Роли и тарифы — только в app_metadata или в отдельной таблице, которую пишет сервер.
  • Представления. Обычный view выполняется с правами владельца и обходит RLS базовых таблиц. На Postgres 15+ нужен with (security_invoker = true), иначе представление открывает то, что таблица закрывает.
  • Функции security definer. Их можно вызвать через RPC, и внутри RLS не действует. Каждая такая функция должна сама проверять, кто её вызвал.
  • Storage. Файлы в бакетах защищаются политиками на storage.objects, отдельно от таблиц. Закрытые таблицы и открытый бакет со сканами паспортов — обычная история.

Как проверить

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

Первый способ — через клиент. Заведи двух тестовых пользователей, из-под первого создай записи, из-под второго попробуй прочитать, изменить и удалить их по идентификатору. Затем попробуй вставить строку с user_id первого. Каждая попытка должна вернуть пустой результат или ошибку.

Второй способ — прямо в SQL, внутри транзакции: set local role authenticated, затем set local request.jwt.claims с нужным sub, затем обычные запросы. Так удобно гонять проверки в CI, не поднимая фронтенд. Для этого есть pgTAP, но хватит и набора SQL-файлов, которые падают при неожиданном результате.

Третье — проверка на полноту. Тест, который перечисляет все таблицы схемы public и падает, если у какой-то выключен RLS или нет ни одной политики, ловит самую частую ошибку — забытую новую таблицу.

Где держать логику, которой мало RLS

Политики хороши для «чьё это». Для «можно ли это сделать сейчас» — хватает ли баланса, не закрыт ли заказ, не превышен ли лимит — им тесно. Такие операции лучше уносить в серверную функцию или edge function с сервисным ключом, а таблицу на запись для клиента закрыть совсем.

Если хочешь, чтобы кто-то со стороны попробовал обойти твои политики до того, как это сделают пользователи, SUBXCAT делает такую проверку и оставляет после неё тесты.

Хочешь, сделаем это за тебя?

Расскажи о проекте ассистенту — через пять минут будет примерная смета, через 48 часов разбор.

Созвон с командой 20 минут: инженер, дизайнер и маркетолог посмотрят на проект вместе с тобой.
Записаться на звонок