Основи безпеки
"Infinite recursion detected in policy" без вимкнення RLS
"Infinite recursion detected in policy for relation" означає, що ваша policy запитала таблицю, яку вона захищає. Ось як розірвати коло.

Коротко
- "Infinite recursion detected in policy for relation" це помилка Postgres 42P17. Правило Row Level Security на таблиці пішло поставити питання цій самій таблиці, і це питання не має кінця.
- Це не ознака того, що Row Level Security не підходив вашому застосунку. Це ознака того, що одне правило запитало по колу.
- Невелика допоміжна функція читає таблицю поза правилами і розриває коло. Вимкнення Row Level Security на цій таблиці теж зупиняє помилку, віддаючи кожен її рядок усім, хто має ключ, що їде всередині вашого застосунку.
Ви увімкнули Row Level Security, написали правило, щоб адміністратори бачили всі профілі, а решта лише свій, і тепер жоден екран вашого застосунку не завантажується. Кожне читання повертається з тим самим рядком:
infinite recursion detected in policy for relation "profiles"
Більшість відповідей, які ви знайдете, кажуть одне й те саме, і це хибна порада: вимкніть Row Level Security на цій таблиці, доки не розберетеся. Помилка справді зупиняється. Зупиняє її те, що таблицю тепер може прочитати будь-хто, хто завітає на ваш сайт.
Що означає "infinite recursion detected in policy for relation"
Правило на таблиці мусило прочитати цю саму таблицю, перш ніж змогло відповісти.
Уявіть швейцара, який звіряє кожного відвідувача зі списком гостей, а список лежить усередині зали, яку він охороняє. Щоб прочитати список, треба зайти. Щоб зайти, треба звіритися зі списком. Першого кроку немає, тож він стоїть у коридорі і ніхто нікуди не потрапляє.
Саме в це впоролася ваша база даних. У неї попросили кілька рядків із
profiles, вона пішла до правила на profiles, щоб зʼясувати, які з них вам
дозволено бачити, а правило сказало їй заглянути в profiles. Postgres
помічає, що ходить по колу, здається і піднімає помилку з кодом 42P17.
Supabase передає її вашому застосунку без змін, тому вона й читається як
механіка.
Запит падає для всіх, до кого застосовується правило, тож це зазвичай приходить як цілий застосунок, що разом лишається порожнім, а не як один зламаний екран.
Коло, намальоване
Правило, яке це спричиняє, пишуть першим майже всі, бо воно каже рівно те, що ви маєте на увазі:
-- Відхилити: це читає profiles, щоб вирішити, кому можна читати profiles.
create policy "admins read every profile" on profiles for select
to authenticated
using (
exists (
select 1 from profiles
where id = (select auth.uid()) and role = 'admin'
)
);
exists (select 1 from profiles …) і є вся проблема. Прочитати profiles
означає застосувати правило на profiles, яке знову виконує
exists (select 1 from profiles …).
Власний посібник Supabase з Row Level Security називає варіант із двома таблицями: policy на двох таблицях, які читають одна одну, ніколи не розвʼязуються, і запит падає для кожної ролі, до якої ці policy застосовуються. Таблиця, що читає саму себе, це та сама форма без гака.
Чому це завжди стається на таблиці profiles?
Бо в profiles лежить те, хто ця людина.
Будь-яке правило, що ставиться до одного типу людей інакше, ніж до іншого, мусить
зʼясувати, якого типу той, хто звертається, а цей факт живе в колонці якоїсь
таблиці. Хоч би як та таблиця звалася, правило, що її захищає, зрештою читає
саме її. У застосунку з Lovable, Bolt, v0 чи Replit це зазвичай profiles, бо
туди потрапляє рядок про кожну людину, яка увійшла. Правило про команди,
організації чи спільні документи дає те саме коло на тій таблиці, що тримає
належність.
Отже, рекурсія виходить із того, що ви написали очевидне правило про єдину таблицю, яка має відповісти на питання про саму себе.
Виправлення: помічник, що читає таблицю поза правилами
Перенесіть питання в невелику функцію, яка виконується як власник бази даних,
щоб читання profiles заради відповіді не йшло через правило на profiles.
create schema if not exists private;
create function private.is_admin()
returns boolean
language sql
security definer -- виконується як роль, що її створила
set search_path = '' -- тож кожне імʼя всередині треба писати повністю
stable
as $$
select exists (
select 1 from public.profiles
where id = (select auth.uid()) and role = 'admin'
);
$$;
revoke execute on function private.is_admin() from public;
grant usage on schema private to authenticated;
grant execute on function private.is_admin() to authenticated;
-- Тепер правило запитує функцію замість таблиці.
create policy "admins read every profile" on profiles for select
to authenticated
using ( (select private.is_admin()) );
Тепер у швейцара список гостей лежить на столі в коридорі. Він читає його, не відчиняючи дверей, а двері лишаються зачиненими для всіх, кого в списку немає.
Чотири рядки там роблять конкретну роботу, і три з них це різниця між виправленням і новою діркою:
security definerце те, що розриває коло. Посібник Supabase визначає це як функцію, що виконується під тією самою роллю, яка її створила, а в Supabase ця рольpostgres, якій дозволено читати понад правилами.set search_path = ''належить кожній із них. Supabase радить ставити це на кожній функції security definer і писати назви таблиць повністю, бо без закріпленогоsearch_pathтой, хто звертається, може спрямувати некваліфіковане імʼя на власний обʼєкт і виконати його з правами власника функції. Саме тому функція пишеpublic.profilesповністю.- Схема
privateважлива з тієї самої причини. Посібник Supabase попереджає, що функцію security definer у відкритій схемі можна викликати через Data API з правами того, хто її створив, і каже ніколи не створювати таку в схемі, що перелічена під Exposed schemas у налаштуваннях вашого API. (select private.is_admin()), загорнуте у власний select. Це дає Postgres порахувати відповідь один раз на всю інструкцію замість одного разу на рядок, що Supabase документує як причину так само загортатиauth.uid().
Рядки revoke і grant лишають функцію доступною для користувачів, які
увійшли, і більше ні для кого.
Ця policy охоплює читання. Якщо збереження почне падати щойно ваші екрани знову наповняться, ви зустріли інше повідомлення з іншою причиною.
Виправлення, яке нічого не виправляє
Вимкнення Row Level Security на profiles теж зупиняє помилку, одним кліком, і
віддає кожен рядок цієї таблиці будь-кому, хто має ключ, який ваш застосунок
віддає браузеру.
Цей ключ задуманий як публічний. Він лежить у коді вашого сайту, і будь-хто дістане його за кілька секунд. Тим, що досі не давало йому повернути всю вашу таблицю користувачів, були правила, які ви щойно вимкнули.
Ваш застосунок після цього виглядає однаково в обох випадках, і саме це робить проблему живучою. Ніщо на ваших екранах не каже, який із двох варіантів ви обрали, і помилки немає в обох.
А каже про це ваша база даних, запитана ззовні. Ми просканували 31 056 живих застосунків, зроблених із Lovable, Bolt, v0, Replit та іншими, і побачили проєкт Supabase за 8 435 із них. Ось що знайшла перевірка Row Level Security:
| Що ми питали | Застосунків |
|---|---|
| Мали видимий проєкт Supabase | 8 435 |
| Їх узагалі можна було запитати | 3 680 |
| Повернули рядки на запит без входу | 2 096 |
| З них із таблиці про людей, замовлення чи повідомлення | 394 |
Середній рядок найчесніший. На 4 755 цих застосунків перевірка не дістала відповіді, і ми це кажемо, замість оголошувати їх чистими. А деякі з 2 096 і мають бути доступними для читання, бо публічна таблиця це цілком бажана річ; ззовні меню і список учасників виглядають однаково.
Ми не можемо сказати вам, скільки з цих таблиць відкрив хтось, хто прибирав саме цю помилку. Ми можемо сказати, що відкрита таблиця це звичайний підсумок поради, яка стоїть першою у результатах пошуку.
Якщо ви радше не хочете про це міркувати, наше безкоштовне сканування читає ваш живий сайт і каже, які з ваших таблиць відповідають сторонній людині. Воно триває близько 20 секунд і не потребує облікового запису: просканувати застосунок.
Коли ви це вимикаєте, стається ще одне. Власний advisor Supabase починає повідомляти про таблицю, це попередження RLS disabled in public, і за місяць воно все ще буде там.
Як зрозуміти, чи коло справді зникло
Те, що помилка зупинилася, не є перевіркою, бо зупиняють її дві різні речі.
Є ще спосіб, у який допоміжна функція виглядає правильно і далі рекурсує, і
Supabase це документує: функція security definer обходить правила лише тоді,
коли це дозволено її власникові. У Supabase власник це postgres, який таке
право має, тож наведений вище взірець працює. Функція, що належить ролі без
цього права, або та, що читає таблицю з force row level security, знову
проходить через правило і лишається в колі.
Дві перевірки, і зробіть обидві:
Запишіть випадки і запустіть їх. Процедура Supabase це файл .sql у
supabase/tests/, який стверджує дозвіл і відмову для кожної операції, для
користувача з входом і для анонімного, запущений через supabase test db.
Admin має бачити всі профілі, учасник свій, стороння людина нічого. Доки це не
проходить, ви знаєте тільки те, що помилка зупинилася.
А потім запитайте ззовні, без входу. Це те питання, яке ставить браузер відвідувача, і єдине, що відображає те, що ваш застосунок справді видає. Чи може стороння людина прочитати вашу базу проходить це крок за кроком, а Row Level Security може бути увімкненим, а таблиця лишатися публічною охоплює випадок, коли правила є і все одно пропускають усіх.
Коли рекурсія каже, що схема хибна
Іноді кругового питання немає що прибирати, бо дві таблиці справді залежать одна від одної.
Посібник Supabase бере спільний доступ за приклад: правило на lists звіряє
list_members, щоб побачити, з ким поділилися списком, а правило на
list_members звіряє lists, щоб побачити, чий він. Правило кожної таблиці
читає іншу, і жодна не може піти першою. Той самий помічник це розвʼязує:
функція повертає id списків, до яких належить той, хто звертається, і обидва
правила запитують цю функцію замість того, щоб питати одне одного.
Сигнал, який варто помітити, це коли вам потрібні три або чотири таких, щоб схема працювала. На цьому етапі належність щоразу перебудовується всередині правил, а одна таблиця, яка прямо каже, кому що видно, зазвичай потребує менше догляду, ніж правила, які ви розплутуєте.
Як тримати таблицю закритою після виправлення
Правило, яке ви виправили сьогодні, описує сьогоднішню базу даних. Наступна policy, наступна таблиця і наступний раз, коли хтось опівночі прибирає помилку, стаються вже після того, як ви востаннє дивилися, і жодна з цих подій не змінює нічого, що видно на ваших екранах.
Reeve Monitor ставить вашому застосунку ті самі питання знову, за розкладом:
- усі девʼять перевірок щогодини, для трьох застосунків
- повідомлення, коли результат змінюється, щоб таблиця, яка відкрилася вночі, не чекала, доки ви помітите
- чи застосунок відповідає, кожні 60 секунд
- щомісячний звіт про побачене
Якщо ваш застосунок тримає дані у вашому власному проєкті Supabase, Reeve Care ще й зберігає копію цієї бази даних. Правило, яке дає сторонній людині читати, це одна проблема. Правило, яке дає їй писати, це інша, і жодна перевірка з цієї статті не поверне видалені рядки.
- зашифрована копія вашої бази Supabase щоночі, збережена там, куди ваш проєкт не дістане
- кожна копія перевірена, перш ніж її зараховано, підрахунком рядків у кожній таблиці
- відновлення одним кліком, коли воно знадобиться
- ваші завантажені файли теж, щойно ви підключите облікові дані Storage
- усе, що робить Monitor
Що зробити зараз
Що робити
- Лишіть Row Level Security увімкненим. Помилка стосується одного правила, а вимкнення правил це зміна всієї таблиці.
- Перенесіть перевірку ролі у функцію
security definerу схемі, яка не відкрита, ізset search_path = ''і кожною назвою таблиці, написаною повністю. - Спрямуйте policy на функцію, загорнуту як
(select private.is_admin()), щоб вона виконувалася раз на інструкцію, а не раз на рядок. - Запишіть дозволені та заборонені випадки у
supabase/tests/і запустітьsupabase test db: admin бачить усі профілі, учасник свій, стороння людина нічого. - Якщо ви вже вимкнули Row Level Security, щоб зрушити з місця, увімкніть його сьогодні і полагодьте правило як слід. Advisor Supabase повідомлятиме про цю таблицю, доки ви цього не зробите, і він належить кожній вашій таблиці.
Якщо вам потрібен решта списку для щойно запущеного застосунку, 10-хвилинний чекліст безпеки охоплює це та інші речі, які варто закрити, перш ніж їх хтось знайде.
Поширені запитання
Що спричиняє "infinite recursion detected in policy for relation"?
Правило на таблиці, яке мусить прочитати цю саму таблицю, перш ніж зможе відповісти. Ваше правило по суті каже "пропусти цю людину, якщо її рядок у profiles каже, що вона admin", тож база даних іде читати той рядок у profiles, а для цього треба перевірити правило на profiles, яке відсилає її читати рядок знову. Postgres помічає, що ходить по колу, зупиняється і піднімає помилку 42P17. Те саме стається між двома таблицями, чиї правила читають одне одного.
Чи безпечно використовувати функцію security definer у policy?
Так, за двох умов, які Supabase називає у власному посібнику з Row Level Security. Поставте search_path у порожній рядок і пишіть усередині кожне імʼя таблиці повністю, тобто public.profiles, а не profiles. Без цього хтось може спрямувати некваліфіковане імʼя на власний обʼєкт і виконати його з правами власника функції. І створюйте функцію в схемі, яка не відкрита через API, бо функцію security definer у відкритій схемі можна викликати ззовні з правами того, хто її створив.
Чи варто вимкнути RLS, щоб це полагодити?
Це зупиняє помилку і відкриває таблицю. З вимкненим Row Level Security кожен рядок цієї таблиці може прочитати будь-хто, у кого є ключ, який ваш застосунок віддає браузеру, а цей ключ будь-хто дістане з вашого сайту. Ваш застосунок поводиться однаково в обох випадках, тож потім ніщо не скаже вам, який із двох варіантів ви обрали. Допоміжна функція нижче зупиняє помилку і лишає таблицю закритою.
Чому це завжди стається на таблиці profiles?
Бо саме в profiles зазвичай лежить відповідь на питання "хто ця людина". Правило, яке по-іншому ставиться до адміністраторів або до учасників команди, мусить знати, ким є той, хто звертається, і цей факт збережено в profiles. Тож правило, що захищає profiles, зрештою читає profiles. Будь-яка таблиця, що тримає роль або належність того, хто звертається, здатна це спричинити, а в застосунку з Lovable, Bolt чи v0 такою таблицею зазвичай є та, що зветься profiles.
Як перевірити, що моя policy справді працює?
Помилку зупиняють дві різні речі, тож сама по собі зникла помилка каже дуже мало. Запишіть дозволені та заборонені випадки у файл .sql у supabase/tests/ і запустіть supabase test db, це процедура, яку Supabase публікує разом із посібником: власник із входом має пройти, а стороння людина має дістати відмову. Потім поставте те саме питання вашій базі ззовні, геть без входу, так само як це робить браузер відвідувача.