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

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

New row violates row-level security policy в Supabase. Що далі?

"New row violates row-level security policy" означає, що Supabase відмовив у записі. Швидке виправлення знову відкриває таблицю для всіх.

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

Коротко

  • New row violates row-level security policy означає, що ваша база даних перевірила правила тієї таблиці й не знайшла жодного, яке дозволяє рядок, що ви зберігали. Вона відмовила в записі й лишила таблицю такою, якою вона була.
  • Одне повідомлення покриває три різні ситуації: правил немає взагалі, є правило лише про читання, або є правило про запис, якому ваш рядок не відповідає.
  • Відповідь, що очолює будь-який пошук, спиняє помилку, дозволяючи будь-який запис від будь-кого. Правило, яке ви насправді хочете, коштує ще близько двох хвилин.

Щось підказало вам увімкнути Row Level Security. Advisor усередині вашої панелі Supabase, результат сканування, чекліст, хтось у Discord. Ви увімкнули. Тепер ваш застосунок не може зберегти взагалі нічого. Кожна спроба повертається зі словами, що a new row violates row-level security policy:

new row violates row-level security policy for table "profiles"

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

Що означає "new row violates row-level security policy"

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

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

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

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

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

Чому вона виникла саме тоді, коли ви увімкнули Row Level Security

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

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

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

У правила дві половини, і лише одна з них про читання

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

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

Саме друга половина дивує людей, бо це правило про щось, чого ще немає.

Ваш застосунок проситьПеревіряється на рядках, що вже в таблиціПеревіряється на рядку, який пишеться
читати (select)USINGнічого перевіряти
зберегти новий рядок (insert)нічого перевірятиWITH CHECK
змінити рядок (update)USINGWITH CHECK
прибрати рядок (delete)USINGнічого перевіряти
Та сама умова, спрямована на дві різні речі. Під час читання її питають про рядки, які існують; під час збереження про рядок, якого ще не існує.

Політика, написана як FOR SELECT, завжди несе лише першу половину, бо коли хтось читає, немає жодного нового рядка для перевірки. Тож вона ніколи не може дозволити збереження, хоч якою поблажливою вона виглядає. У цьому вся розбіжність, і вона пояснює, чому ваше читання відновилося, а запис ні.

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

Виправлення, яке спиняє помилку, і чого воно коштує

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

Зазвичай воно приходить у такому вигляді:

CREATE POLICY "Enable insert for all users"
  ON public.profiles
  FOR INSERT
  WITH CHECK (true);

Кожен рядок задовольняє true, тож дозволено кожне збереження, і від вашого застосунку, і від будь-кого іншого з ключем, що їде всередині нього. Вимкнути Row Level Security назад робить те саме ще ґрунтовніше.

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

Правило, яке дозволяє зберігати вашому застосунку, і тільки йому

Назвіть, кому належить рядок, і порівняйте це з тим, хто питає:

CREATE POLICY "Users insert their own rows"
  ON public.profiles
  FOR INSERT
  TO authenticated
  WITH CHECK (auth.uid() = user_id);

auth.uid() є тим, хто увійшов і робить запит. user_id є стовпцем на рядку, який каже, кому той належить. Коли ці двоє збігаються, рядок зберігається. Коли не збігаються або коли ніхто не увійшов, у ньому відмовляють, і той, кому відмовили, бачить те саме повідомлення, на яке дивитесь ви.

Дві деталі в тому правилі заслуговують свого місця. TO authenticated означає, що правило стосується відвідувачів, які увійшли; приберіть це, і політика стосуватиметься всіх, разом зі сторонніми. І ваш застосунок має справді надсилати стовпець user_id, бо правило, яке порівнює auth.uid() зі значенням, що прийшло порожнім, ніколи не збігнеться.

Коли правило правильне, а помилка все одно зʼявляється

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

Три різні причини, що доходять до вас одним реченням. Код 42501 однаковий в усіх трьох, і саме тому саме повідомлення не може сказати вам, у якій ви опинилися.

Три пояснення покривають майже все, що лишається:

Ніхто не увійшов. auth.uid() повертається порожнім для відвідувача, який не входив, тож правило, що порівнює його зі стовпцем власника, збігтися не може. Це звична річ для форми реєстрації, списку очікування чи форми звʼязку, і такі форми потребують власного правила, яке описує, що дозволено додати сторонньому.

Стовпець власника так і не приходить. Ваше правило порівнює auth.uid() із user_id, а ваш застосунок надсилає все, крім user_id. Порівняння йде проти порожнечі й щоразу зазнає невдачі.

Запис іде в Storage. Завантажені файли осідають у Supabase Storage, який тримає власні політики на storage.objects, а не на вашій таблиці. Правило, написане на profiles, нічого не каже про файл.

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

Що зробити сьогодні

Що робити

  • Читайте помилку як відмову, а не як несправність. Запис не відбувся, таблиця не змінилася, і відновлювати нічого не треба.
  • Перевірте, чи має таблиця бодай якусь політику про запис. Правило, написане як FOR SELECT, полагодило ваші екрани й нічого не сказало про збереження.
  • Додайте політику FOR INSERT, чия умова WITH CHECK називає власника рядка, і додайте ту клаузу TO, яку ви мали на увазі.
  • Переконайтеся, що ваш застосунок справді надсилає стовпець власника, і спробуйте зберегти ще раз, увійшовши в систему.
  • Лишіть WITH CHECK (true) для таблиць, вміст яких вас влаштував би на публічній сторінці. Для всього, де є людина, витратьте ті дві хвилини.

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

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

Що означає "new row violates row-level security policy"?

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

Я додав політику, читання працює. Чому збереження досі не вдається?

Бо політика про читання нічого не каже про запис. Правило несе половину USING, яку база даних застосовує до рядків, що вже є в таблиці, і половину WITH CHECK, яку вона застосовує до рядка, що ви намагаєтесь створити. Політика, написана як FOR SELECT, завжди має лише першу, бо під час читання немає жодного нового рядка для перевірки. Ваші екрани знову наповнюються, а перше збереження далі не вдається. Додайте другу політику FOR INSERT з умовою WITH CHECK.

Може, просто вимкнути Row Level Security, щоб це зникло?

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

Моя політика виглядає правильною, а insert усе одно не проходить. Що це ще може бути?

Три речі пояснюють більшість таких випадків. Ніхто не увійшов, тож auth.uid() повертається порожнім, і правило, яке порівнює його зі стовпцем власника, ніколи не збігнеться, що є звичним для форми реєстрації чи форми звʼязку. Або ваш застосунок узагалі не надсилає стовпець власника, тож правило порівнює із значенням, яке прийшло порожнім. Або запис іде в Supabase Storage, а не в таблицю, і Storage тримає власні політики на storage.objects.

Чи означає ця помилка, що хтось намагався атакувати мій застосунок?

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

Автор

Vlad Tkachenko

Засновник Reeve

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

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

Читати далі

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

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

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

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