Перейти до вмісту

Основи безпеки

Увімкніть Row Level Security на кожній таблиці Supabase і перевірте

Row Level Security у Supabase без політики закриває таблицю повністю. Політика без увімкненого перемикача не робить нічого. Ось SQL, і ось перевірка.

Vlad Tkachenko11 хв читання
Список таблиць бази даних на панелі, біля кожної перемикач безпеки, і більшість із них досі вимкнені.

Коротко

  • Увімкнути Row Level Security у Supabase без політики означає закрити таблицю повністю, а написати політику, не увімкнувши перемикач, означає не зробити нічого. Кожній таблиці потрібне і те, і те.
  • Три форми політик покривають майже все, що створює конструктор зі ШІ: рядки, які належать одній людині, рядки, які може читати будь-хто, і рядки, які ваш застосунок пише від імені відвідувача.
  • Далі перевірте ззовні свого застосунку й без входу, бо саме такий запит робить незнайомець, і лише він каже вам, що ваші політики роблять, а не що вони обіцяють.

Вам сказали увімкнути Row Level Security, або ваш конструктор зі ШІ згадав про це мимохідь, поки лагодив щось інше. У вашому проєкті Supabase десь від чотирьох до сорока таблиць, і ви не знаєте, які з них закриті.

Інструкція, яку ви знайдете всюди, це один рядок SQL на таблицю, і настільки вона правильна. Те, перед чим спиняється майже кожен посібник, це наступний крок. Політика, яка існує, це не політика, яка працює, і ніщо у вашому дашборді не покаже вам різниці. Тож увімкнути Row Level Security у Supabase це три роботи, а не одна: поставити перемикач на всіх таблицях, написати ті дві чи три політики, які збирають ваш застосунок назад, а потім спитати у власної бази те, що спитав би в неї незнайомець.

Що насправді робить увімкнений Row Level Security?

Змушує Postgres звірятися з вашими правилами, перш ніж віддати рядок. З вимкненим перемикачем звірятися немає з чим, тож відповідь на будь-який запит це все.

Уявіть бібліотекаря, який приносить те, що ви попросили. Row Level Security це вказівка прочитати записку про вас, перш ніж наповнювати візок. Записка це ваша політика, і в ній може бути написано, що ця людина може взяти книжки, які сама написала, або що будь-хто може взяти будь-що. Якщо вказівка діє, а записки ще ніхто не написав, бібліотекар повертається з порожнім візком і не пояснює чому.

Саме на цьому останньому люди й спотикаються, і саме воно вирішує, як виглядатиме перевірка згодом. Row Level Security фільтрує рядки. Він не відмовляє в запитах. Таблиця, яку вам не дозволено читати, відповідає порожнім списком і кодом успіху, а не помилкою чи вікном входу. Вашому застосунку ніколи не кажуть, що йому відмовили. Він не отримує нічого й малює порожній екран.

Один запит, три налаштування й той самий код успіху щоразу. Row Level Security змінює те, що повертається, а не те, чи запит вдався.

Тож поставити перемикач на проєкті, у якому ще немає жодної політики, не закриє ваші дані від незнайомців. Це закриє їх від усіх, зокрема від вашого власного застосунку, доки ви не скажете, кому що можна бачити.

Як увімкнути RLS на всіх таблицях Supabase одразу?

Один цикл, виконаний один раз у SQL Editor. Він проходить кожну таблицю вашої схеми public і вмикає перемикач скрізь, де той вимкнений.

Почніть із того, щоб побачити, де ви стоїте. Це виведе ваші таблиці й скаже, чи є перемикач у кожної просто зараз:

select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;

Кожен рядок, де rowsecurity показує false, це таблиця, яка віддає свій вміст будь-кому з публічним ключем, що їде разом із вашим застосунком. Якщо список короткий, робіть по одній і дивіться, що ламається:

alter table public.orders enable row level security;

Якщо він не короткий, це впорається з усіма:

do $$
declare t record;
begin
  for t in
    select tablename from pg_tables where schemaname = 'public'
  loop
    execute format(
      'alter table public.%I enable row level security', t.tablename
    );
  end loop;
end $$;

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

Три політики, які вам справді потрібні

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

Рядки, які належать одній людині. Замовлення, повідомлення, збережені позиції, усе, що має власника.

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

auth.uid() це id того, хто увійшов у цьому запиті. user_id це той стовпець вашої таблиці, у якому записаний власник, тож перевірте назву, перш ніж виконувати: конструктори пишуть також owner_id, profile_id і created_by. Рядок to authenticated означає, що для відвідувача без входу політику навіть не розглядають, і саме це тримає таблицю закритою від публіки.

Рядки, які може читати будь-хто. Каталог товарів, опубліковані статті, карта закладів.

create policy "anyone may read products"
on public.products for select
to anon, authenticated
using ( true );

Цю пишіть свідомо або не пишіть узагалі, бо це ще й та політика, за яку хапається конструктор зі ШІ тієї миті, коли ви просите його полагодити порожній екран. Питання, яке треба вирішити спершу: чи могла б ця таблиця бути сторінкою на вашому сайті, точно такою, як є, нічого не прибираючи? Ні означає, що їй потрібна політика власності вище.

Рядки, які пише ваш застосунок. Читання й запис у Postgres це окремі права, тож таблиця, у яку ваш застосунок зберігає, потребує другої політики, і ця перевіряє рядок на вході, а не на виході.

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

Яка клауза куди йде, це та частина, на якій людей ловить, а рядок update несе вимогу, у якій немає нічого очевидного:

ОпераціяКлаузаПотребує ще
selectusing
insertwith check
updateusing і with checkполітики select на тій самій таблиці
deleteusing

Документація Supabase щодо останньої однозначна: без відповідної політики select update не працює так, як ви очікуєте. А якщо сюди вас привело повідомлення new row violates row-level security policy, то відстань між цими двома клаузами і є вся ця помилка.

Чому в кожному прикладі пишуть (select auth.uid()), а не auth.uid()

Бо дужки змушують Postgres обчислити значення один раз для всього запиту, а не один раз для кожного рядка, який він переглядає.

Загорнута версія стає тим, що Postgres називає initPlan: виконується один раз, а далі використовується повторно до кінця інструкції. Без дужок функцію викликають знову на рядку один, на рядку два, на рядку три, і так далі вниз по таблиці, у якій їх може бути сто тисяч. Власний Performance Advisor у Supabase повідомляє про версію без дужок за правилом під назвою Auth RLS Initialization Plan, і це один із записів, які там трапляються найчастіше.

Дужки і є вся різниця. Ліворуч функція виконується раз на кожен рядок; праворуч вона виконується один раз, а відповідь використовується повторно.

Одне застереження, і воно від самої Supabase: це працює тому, що відповідь не змінюється від рядка до рядка. Функцію, результат якої справді залежить від рядка перед нею, з циклу не винести, тож саме її лишіть без дужок.

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

create index orders_user_id_idx on public.orders (user_id);

Перевірити політику, не створюючи фальшивого акаунта

SQL Editor у Supabase може виконати запит так, ніби його надіслав конкретний відвідувач, і це повністю покриває анонімний випадок.

У редакторі є елемент керування для ролі, під якою має виконуватися запит. Поставте його на анонімну роль, а потім виконайте звичайний select до таблиці, яку ви щойно змінили. Що повернеться, те й отримає незнайомець без входу. Для відвідувача з входом візьміть id користувача, який у вас уже є, замість створювати нового; для перевірки згодиться будь-який рядок вашої таблиці.

Якщо вам зручніше набрати, ніж клацнути, те саме в SQL:

begin;
set local role anon;
select * from public.orders;
rollback;

begin і rollback стоять тут, щоб зміна ролі тривала цей блок і не довше, а це важить, коли ви проходите кілька таблиць за один раз.

Це каже вам, що ваші політики роблять усередині бази. Чого воно сказати не може, так це того, що ваш проєкт віддає в інтернет, бо запит, який ваші відвідувачі роблять насправді, починається не в SQL Editor. Він починається в браузері, несе публічний ключ і приходить через публічну адресу вашого проєкту.

Як перевірити Supabase RLS ззовні свого застосунку?

Надішліть запит, який надіслав би незнайомець. Вам потрібні два значення, і обидва вже лежать у коді, який ваш сайт віддає кожному відвідувачу: URL вашого проєкту й ваш публічний ключ.

curl "https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*&limit=1" \
  -H "apikey: YOUR_PUBLISHABLE_KEY" \
  -H "Authorization: Bearer YOUR_PUBLISHABLE_KEY"

Підставляйте по одній назві таблиці й читайте, що повертається. Порожній список, [], означає, що політика витримала для відвідувача без входу. Рядок означає, що будь-хто з ключем, який їде разом із вашим застосунком, може читати цю таблицю. Помилка, яка згадує сам ключ, означає, що ви скопіювали не те значення, і це варто виключити, перш ніж робити висновки.

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

У новішому проєкті Supabase цей ключ починається з sb_publishable_, а в старішому це ключ anon. Обидва безпечно так використовувати й безпечно тримати у вашому застосунку, у цьому й полягає їхнє призначення; які API-ключі належать фронтенду розбирає ту пару, яка не належить, а де знайти їх у дашборді це чотири значення на тій сторінці налаштувань.

Це та перевірка, яка відповідає дійсності, і саме тому, що ми прогнали її у великому масштабі, ми знаємо, наскільки ця прогалина поширена. Між 12 і 14 серпня 2026 року ми прогнали дев'ять зовнішніх перевірок по 30 998 живих застосунках, опублікованих із Lovable, Base44, Replit, v0 і Bolt. Із 3 680 застосунків на Supabase, де перевірка дійшла до кінця, 2 096 відповіли на анонімний запит рядками щонайменше з однієї таблиці. Це 57 %, і це частка від застосунків, від яких ми отримали чітку відповідь, а не від усього, що ми просканували. Повний набір даних опубліковано, а від чого ці 57 % є часткою проходить підрахунок.

Робити це руками нормально для чотирьох таблиць і виснажливо для сорока. Наше безкоштовне сканування надсилає цей запит за вас, з'ясовує, які таблиці існують, без того щоб ви їх називали, і каже, які відповіли, поряд із вісьмома іншими перевірками, які воно робить ззовні. Воно читає ваш живий сайт так само, як може будь-який відвідувач, триває близько 20 секунд і не потребує акаунта: просканувати застосунок.

Перш ніж переписувати політики на живій таблиці

Спершу зробіть копію своєї бази. Ви збираєтеся змінити права на всіх таблицях, які маєте, тими самими інструментами, що створили проблему.

Варто розділити два різні ризики, бо лише один із них стосується сьогоднішньої роботи. Якщо таблицю міг читати й змінювати будь-хто, то закрити її зараз нічого не змінить у тому, що вже сталося, і ця версія зазвичай спливає як звернення до підтримки про дані, які змінилися самі. Другий ризик це сама міграція. Політика, яку видалили й трохи криво створили заново, це звичайний вівторок, а шлях назад це копія того, як усе виглядало годину тому.

На платному тарифі Supabase учорашня копія чекає в консолі. На безкоштовному тарифі відступати немає куди взагалі, бо Supabase не робить жодних автоматичних резервних копій на безкоштовному тарифі. Якщо це про вас, зробіть копію, перш ніж починати.

Reeve Care це та версія цього, про яку вам не треба пам'ятати. Вашу базу Supabase копіюють за розкладом, тримають поза вашим акаунтом Supabase, зашифрованою, і зчитують назад, щоб підтвердити, що вона відновлюється, перш ніж дата на вашому дашборді зрушить. Завантажені файли їдуть разом із нею, щойно ви під'єднаєте ключ Storage, і цей ключ просять окремо, бо Supabase не видає ключа тільки для читання для файлів: той, що копіює ваші завантаження, вміє й писати, а той, що копіює вашу базу, не вміє. Під'єднувати його необов'язково, і базу резервують у будь-якому разі. Резервні копії лише для Supabase, тож якщо ваші дані живуть деінде, ми так і кажемо, замість продавати підписку, яка стереже порожню коробку.

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

Друга половина це та перевірка, яку ви щойно зробили руками. Політику, яка послабилася під час якоїсь пізнішої міграції, ніхто не знайде поглядом, тож Care прогонить той самий анонімний запит знову за розкладом і скаже вам, коли почне відповідати таблиця, яка минулого тижня мовчала. Моніторинг доступності та щомісячний звіт входять у ту саму підписку. Ніщо з цього не напише ваші політики за вас, і жодна резервна копія не закриє відкриту таблицю. Змінюється те, скільки коштуватиме вам міграція, що пішла не так. Що Reeve резервує в Supabase, як часто, і що робить відновлення проходить увесь цикл, а тарифи є на сторінці цін.

Порядок, у якому працювати

Що робити

  • Виведіть свої таблиці через select tablename, rowsecurity from pg_tables where schemaname = 'public' і подивіться, скільки з них відкриті, перш ніж щось міняти.
  • Зробіть резервну копію, а потім увімкніть Row Level Security на кожній таблиці схеми public. Очікуйте, що застосунок стане порожнім; це перемикач робить свою роботу.
  • Дайте кожній таблиці одну з трьох політик. Власність це типовий випадок; USING (true) це рішення, яке ви приймаєте таблиця за таблицею, а не спосіб повернути екрани.
  • Пишіть (select auth.uid()), а не auth.uid(), і додайте індекс на стовпець, за яким фільтрують ваші політики.
  • Перевірте кожну таблицю ззовні й без входу. Порожній список це залік. Рядок це таблиця, яку може прочитати будь-хто, хоч би що казав ваш дашборд.

Почніть із тієї таблиці, за яку вам було б найсоромніше як за публічну сторінку, і спускайтеся звідти вниз. 10-хвилинний чекліст безпеки охоплює це разом з рештою того, що варто підтвердити у щойно запущеному застосунку, а посібник з безпеки Supabase проходить по тому, що ще схильне лишатися відкритим.

Поширені запитання

Що станеться, якщо я увімкну RLS і не напишу жодної політики?

Таблиця перестане відповідати, зокрема й вашому власному застосунку. З увімкненим перемикачем Postgres звіряється з вашими політиками, перш ніж віддати рядок, а без політик немає нічого, що могло б сказати так, тож запити повертаються порожніми. Supabase пише про це прямо: дані стають недоступними через API з публічним ключем, доки не визначено політики. Нічого не видаляється і нічого не ламається. Напишіть політику для тих рядків, які ваш застосунок має показувати, і екрани повернуться.

Чи сповільнює Row Level Security мої запити?

Може сповільнювати, і обидва виправлення невеликі. Пишіть `(select auth.uid())` в умові політики, з дужками, щоб Postgres обчислив значення один раз для всього запиту й далі використовував його для кожного рядка. Supabase позначає версію без дужок у власному Performance Advisor, за правилом під назвою Auth RLS Initialization Plan. Далі додайте індекс на стовпець, за яким фільтрує політика, зазвичай це `user_id`. На таблиці в кілька тисяч рядків ви навряд чи помітите різницю. На великій важить і те, і те.

Чи потрібен RLS, якщо в таблиці немає персональних даних?

Увімкненим його все одно варто мати, а політика може бути дозвільною. Таблицю з вимкненим Row Level Security може прочитати будь-хто з публічним ключем, який їде разом із вашим застосунком, і це нормально для каталогу товарів та значно менш нормально для всього, що ви не опублікували б окремою сторінкою. Увімкнути його й написати політику читання з `USING (true)` дає той самий публічний доступ, але свідомо, і наступного разу, коли хтось переглядатиме список, ця таблиця читатиметься як вирішена, а не як забута.

Чому моя realtime-підписка перестала працювати після увімкнення RLS?

Бо Realtime звіряється з тими самими політиками, перш ніж надіслати зміну підписнику. Таблиця з увімкненим Row Level Security і без політики select для цього відвідувача не віддає рядків запиту й не віддає змін підписці, рівно з тієї самої причини. Додайте політику select, потрібну цій підписці, і потік відновиться. Якщо ви користуєтеся Realtime broadcast чи presence, а не змінами в базі, вони авторизуються окремо, політиками на таблиці `realtime.messages`.

Як перевірити політику, не створюючи фальшивого користувача?

Двома способами, і жоден не потребує нового акаунта. SQL Editor у Supabase може виконати запит так, ніби його надіслала конкретна роль, і це повністю покриває анонімний випадок: що повернеться, те й отримає незнайомець. Для випадку з входом потрібен id користувача як дублер, і згодиться будь-який рядок, який уже є у вашій таблиці. Другий спосіб це запит ззовні з вашим публічним ключем, який перевіряє весь шлях, а не саму лише політику.

Автор

Vlad Tkachenko

Засновник Reeve

Я щодня дивлюся на застосунки, зібрані в Lovable, Bolt, v0, Cursor і Replit, і на короткий список помилок, які трапляються в них знову і знову.

Більше про автора

Читати далі

Усі статті

Не впевнені, як справи у вашому застосунку?

Запустіть безкоштовне сканування й отримайте зрозумілу оцінку від A до F приблизно за 20 секунд. Без облікового запису й без картки.

Сканувати безкоштовно

Автоматична зовнішня перевірка, а не повний аудит. Відсутність знахідок не є гарантією безпеки.