Практична безпека · Supabase

Supabase RLS: як захистити дані користувачів

RLS вирішує просте, але критичне питання: чому користувач А не може прочитати або змінити дані користувача Б, навіть якщо вручну надішле запит до вашого API.

23 вересня 202615 хв читанняСкладність: початкова → середня
Що буде на виході. Створимо таблицю приватних нотаток, дамо авторизованій людині доступ лише до власних рядків, закриємо анонімні запити й складемо тестову матрицю. Потрібні проєкт Supabase, увімкнений Auth і доступ до SQL Editor або міграцій.

RLS — це фільтр у самій базі даних

Row Level Security, або захист на рівні рядків, — механізм PostgreSQL. Політика прикріплюється до таблиці й перевіряється щоразу, коли запит читає, створює, змінює або видаляє дані. Її зручно уявляти як додаткову умову WHERE, яку база застосовує незалежно від фронтенду.

Це важливо для вайб-кодингу. AI може сховати чужі записи у кнопках та інтерфейсі, але прихована кнопка не є захистом. Користувач здатен відкрити DevTools, змінити ID у запиті або звернутися до Data API напряму. Якщо база не перевіряє власника рядка, красивий застосунок залишає дані відкритими.

Коли RLS увімкнено, але дозволяючої політики немає, для звичайних клієнтських ролей діє принцип default deny: рядок недоступний. Власник таблиці та ролі з bypassrls — окремий випадок. Тому успішний запит у SQL Editor ще не доводить, що правила працюють для користувача.

Три рівні, які не можна плутати

РівеньЩо вирішуєТипова помилка
GRANTЧи може роль узагалі виконувати SELECT, INSERT, UPDATE або DELETE.Політика правильна, але операція падає з 42501, бо права не видані.
RLS-політикаДо яких саме рядків дозволена операція.using (true) випадково відкриває всі рядки ролі, що вже має GRANT.
Логіка застосункуЩо показати користувачеві та який запит відправити.Інтерфейс приховує дані, але база віддає їх прямим запитом.

Офіційна документація Supabase радить налаштовувати GRANT і RLS разом. Політика не відкликає зайвих прав автоматично. Надійний підхід: забрати всі клієнтські дозволи, повернути лише потрібні операції, а потім обмежити рядки окремими політиками.

Крок 1. Створюємо таблицю й закриваємо зайве

Приклад — приватні нотатки. Поле user_id посилається на Supabase Auth, не допускає null і має індекс. Код краще зберегти як міграцію, щоб правила відтворювалися на тестовому та бойовому середовищах.

create table public.notes (
  id bigint generated always as identity primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  body text not null,
  created_at timestamptz not null default now()
);

create index notes_user_id_idx on public.notes(user_id);

alter table public.notes enable row level security;

revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete
  on table public.notes to authenticated;

Роль anon нічого не отримала, бо нотатки приватні. Роль authenticated має чотири операції, але ще не має доступу до жодного рядка: його дадуть політики. Якщо таблиці потрібне лише читання, не видавайте запис «про запас».

Крок 2. Окрема політика для кожної операції

Supabase Auth додає до запиту ідентифікатор користувача. Функція auth.uid() повертає його UUID, а без чинної сесії — null. Обмеження to authenticated відсікає запити неавторизованої ролі.

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

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

create policy "Users update own notes"
on public.notes for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);

create policy "Users delete own notes"
on public.notes for delete
to authenticated
using ((select auth.uid()) = user_id);

USING перевіряє наявний рядок: чи можна його побачити, змінити або видалити. WITH CHECK перевіряє новий стан. Тому UPDATE має обидві умови: користувач може взяти лише власну нотатку й не може під час редагування перепризначити її іншому UUID.

Поширена пастка: для UPDATE потрібна відповідна SELECT-політика. Без можливості побачити рядок оновлення може не спрацювати, навіть якщо UPDATE-політика виглядає правильно.

Крок 3. Перевіряємо дозволені й заборонені сценарії

Найнебезпечніший тест — увійти власником, побачити свою нотатку й вирішити, що все гаразд. Захист доведений лише тоді, коли інший користувач не може отримати той самий рядок. Створіть два тестові акаунти: А і Б, без реальних персональних даних.

СценарійОчікуваний результат
Гість без сесії читає notesНемає доступу: для anon не видано GRANT і політику.
А створює рядок зі своїм user_idУспіх.
А підставляє UUID користувача БINSERT відхилено через WITH CHECK.
Б запитує ID нотатки АРядок не повертається.
Б змінює або видаляє нотатку АЖоден чужий рядок не змінений і не видалений.
А змінює user_id нотатки на БUPDATE відхилено через WITH CHECK.

Для повторюваної перевірки Supabase рекомендує SQL-тести в supabase/tests/:

supabase test new notes_rls.test
supabase test db

У тестах перевіряйте чотири операції для власника, стороннього користувача та гостя. Після зміни схеми запускайте Security Advisor: він знаходить RLS, вимкнений у public, небезпечні views, посилання на змінювані метадані користувача та інші конкретні проблеми.

Сім помилок, через які RLS не рятує

  1. RLS увімкнули не на всіх відкритих таблицях. Одна допоміжна таблиця в exposed-схемі може віддати зв’язки, email або внутрішні статуси.
  2. Залишили using (true). Для SELECT це означає «усі рядки» для вказаної ролі. Так можна робити лише зі справді публічними даними.
  3. Перевіряють роль у user_metadata. Користувач може змінювати ці метадані через Auth API. Авторизаційні дані тримайте в захищеній таблиці або app_metadata.
  4. Вставили secret чи service_role у фронтенд. Такий ключ обходить RLS. Його витік потрібно негайно відкликати.
  5. Тестують лише через SQL Editor. Адміністративна роль може обходити RLS. Тестуйте реальну роль, JWT і двох користувачів.
  6. Захистили таблицю, але відкрили view. Звичайне представлення часто виконується з правами власника й може обійти політики базових таблиць.
  7. Додали політику, але забули GRANT. Або видали всі операції, а рядки обмежили надто широкою умовою. Це різні перевірки.

Views, ролі й командна робота

У PostgreSQL 15+ для view, яка має виконувати політики базових таблиць від імені користувача, застосовуйте security_invoker:

create view public.my_note_counts
with (security_invoker = true)
as
select user_id, count(*) as total
from public.notes
group by user_id;

Не будуйте ролі лише всередині JWT без плану оновлення сесій: токен не змінюється миттєво після редагування app_metadata. Для чутливих змін враховуйте затримку до оновлення JWT або перевіряйте стан у таблиці.

security definer-функції іноді потрібні для складної командної моделі й уникнення рекурсивних політик, але вони виконуються з правами власника. Тримайте їх у невідкритій схемі, задавайте search_path = '', пишіть повні назви схем і видавайте EXECUTE лише потрібній ролі. Для першого проєкту безпечніше почати з прямих політик власника.

Щоб політики не сповільнили велику таблицю

Безпека спочатку, оптимізація після правильності. Але дві речі варто зробити одразу. По-перше, індексуйте колонки, за якими фільтрує політика: у прикладі це user_id. По-друге, використовуйте (select auth.uid()), якщо значення не залежить від рядка. PostgreSQL може обчислити helper один раз на запит, а не повторювати для кожного рядка.

Не вимикайте RLS заради швидкості. Перевірте план запиту, індекси та Performance Advisor. Якщо політика робить вкладені перевірки членства в командах, індексуйте зовнішні ключі й колонки зв’язку.

Промпт для перевірки AI-згенерованої схеми

Передавайте асистенту структуру без реальних записів, ключів і персональних даних. Просіть не просто «додати RLS», а скласти модель загроз і негативні тести.

Промпт для рев’юПеревір цю схему Supabase/PostgreSQL як security reviewer. Для кожної таблиці в exposed-схемі склади матрицю ролей і операцій SELECT, INSERT, UPDATE, DELETE. Перевір: чи ввімкнено RLS; чи мінімальні GRANT; чи USING обмежує старий рядок; чи WITH CHECK обмежує новий; чи не можна змінити owner_id; чи немає доступу через view або security definer function; чи secret/service_role не використовується у браузері. Запропонуй окремі allow і deny тести для гостя, власника та іншого користувача. Не вигадуй назви колонок: спочатку переліч усі припущення.

Чекліст перед production

  • RLS увімкнено на кожній таблиці, доступній через Data API.
  • anon і authenticated мають лише необхідні GRANT.
  • Для SELECT, INSERT, UPDATE і DELETE створено явні політики.
  • Власник не може перепризначити рядок іншому користувачеві.
  • Інший користувач не читає й не змінює чужі рядки.
  • Secret і service_role існують лише на сервері.
  • Views та security definer-функції перевірені окремо.
  • Колонки політик та зв’язків проіндексовані.
  • Security Advisor не показує невиправлених warning або error.
  • RLS-тести запускаються після кожної зміни міграцій.

Короткі відповіді

Що таке RLS у Supabase?

Це правила PostgreSQL, які визначають, які рядки може читати або змінювати конкретний запит. Вони працюють у базі, а не лише в інтерфейсі.

Чи безпечний публічний ключ Supabase у браузері?

Publishable або legacy anon key розрахований на клієнт за умови правильних RLS і GRANT. Secret або service_role обходить RLS і ніколи не повинен потрапляти у браузер. Докладніше — у матеріалі «Як не злити API-ключі».

Чому UPDATE не працює з правильною політикою?

Для UPDATE потрібна також SELECT-політика. USING перевіряє існуючий рядок, WITH CHECK — результат зміни. Якщо все правильно, перевірте GRANT.

Як перевірити Supabase RLS?

Перевірте гостя, власника та іншого користувача для всіх чотирьох операцій. Відтворюваний варіант — SQL-тести в supabase/tests/ і команда supabase test db.

Першоджерела

  1. Supabase Docs — Row Level Security, актуальна документація з GRANT, політиками й тестами.
  2. Supabase Docs — API keys, publishable та secret keys.
  3. Supabase Docs — Security and Performance Advisors.
  4. PostgreSQL 18 Documentation — Row Security Policies.

Сумніваєшся у своїх політиках?

Опиши структуру таблиць без ключів і приватних даних у Telegram-чаті. Допоможемо скласти матрицю доступу й негативні тести, але фінальну перевірку роби на окремому середовищі.

Обговорити в Telegram ↗
Обговорити в Telegram ↗