GuardLabs
demo / supabase-rls
Supabase · Row Level Security

RLS в Supabase: политика, которая молчит, опаснее политики, которая падает

Row Level Security не бросает ошибок. Сломанная политика либо молча отдаёт чужие данные, либо молча возвращает пустой список, и оба случая выглядят как «код работает». Ниже оба провала показаны вживую на настоящем PostgreSQL 16, тем же механизмом, каким Supabase исполняет запросы клиентов.

Боль

Два молчаливых провала RLS

Молча пропускает лишнее

  • Политика написана, а ALTER TABLE ... ENABLE ROW LEVEL SECURITY забыт: Postgres игнорирует политику целиком и отдаёт все строки.
  • В интерфейсе Supabase таблица выглядит защищённой: политика ведь существует и видна в списке.
  • Утечку замечает не разработчик, а клиент, который увидел чужие заказы в своём кабинете.

Молча режет всё

  • RLS включён, но политика написана только TO authenticated: аноним получает пустой список без единой ошибки.
  • Классический симптом: «в таблице есть данные, а select возвращает ноль строк, и логи чистые».
  • Та же пустота возникает, когда в JWT нет sub и auth.uid() возвращает NULL: сравнение с NULL отбрасывает все строки.

Живой артефакт

Прогоните запрос от трёх личностей на трёх версиях политик

Это не симуляция. Кнопка исполняет настоящий SELECT в PostgreSQL 16 на этом сервере: SET LOCAL ROLE, затем request.jwt.claims с sub пользователя, затем запрос. Ровно так это делает PostgREST внутри Supabase, включая функцию auth.uid().

Версия политик
Кто спрашивает

Строки, которые вернул Postgres

Выберите версию политик и личность, нажмите кнопку.
исполненный SQL появится здесь

В таблице четыре заказа: два принадлежат Alice, два принадлежат Bob. Заказы №1 и №3 помечены как публичные. Красная строка означает данные, которые этой личности видеть нельзя.

Решение

Как мы закрываем RLS так, чтобы он не молчал

Правила политик

  • ENABLE плюс FORCE ROW LEVEL SECURITY на каждой таблице с пользовательскими данными: FORCE закрывает даже владельца таблицы.
  • Отдельная политика на каждую роль и каждое действие: anon и authenticated, select, insert, update, delete. Дыра почти всегда там, где роль «забыли».
  • Insert и update всегда с WITH CHECK: USING защищает чтение, но не запись.

Тесты политик как код

  • Матрица «личность x таблица x ожидаемые строки» прогоняется скриптом после каждой миграции: ровно то, что делает кнопка выше.
  • Проверяем обе стороны: пользователь видит своё И не видит чужого. Половина тестов обычно проверяет только первое.
  • Миграции политик храним в файлах рядом с миграциями схемы, а не правим руками в дашборде.

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

Те же запросы из терминала

# сломанная версия: залогиненная Alice видит чужие заказы Bob
curl "https://guardlabs.online/demo/supabase-rls/api/run?scenario=leak&who=alice"

# сломанная версия: аноним получает пустоту без ошибки
curl "https://guardlabs.online/demo/supabase-rls/api/run?scenario=block&who=anon"

# исправленная версия: аноним видит только публичные строки
curl "https://guardlabs.online/demo/supabase-rls/api/run?scenario=ok&who=anon"

Ответ содержит исполненный SQL и строки как есть из базы. Эндпоинт read-only: сценарий и личность выбираются из фиксированного списка, произвольный SQL с фронта не принимается.