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

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

"Row-level security policy for table objects" при вивантаженні

"New row violates row-level security policy for table objects" означає, що вашому вивантаженню бракує правила insert. Публічний бакет його не додає.

Vlad Tkachenko8 хв читання
Віконце прийому із засувом і документ, що лишився назовні, а позаду стелаж із файлами з кришкою над верхнім краєм.

Коротко

  • "New row violates row-level security policy for table objects" означає, що ваше вивантаження дійшло до Supabase Storage і жодне правило на storage.objects не пустило файл усередину. Файл не збережено, а решта бакета лишилася недоторканою.
  • Storage тримає свої правила на одній таблиці для всіх бакетів вашого проєкту, і саме тому повідомлення називає таблицю, якої ви ніколи не створювали.
  • Зробити бакет публічним це не виправить. Supabase перевіряє вивантаження в обох випадках, а публічність вирішує тільки те, хто може відкрити файл, адресу якого вже має.
  • Вивантаженню потрібне правило insert на storage.objects. Якщо ваш код зберігає через upsert, йому потрібні ще select і update.

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

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

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

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

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

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

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

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

Чому повідомлення називає "objects", а не ваш бакет

Бо Storage тримає правила для всіх бакетів вашого проєкту на цій одній таблиці.

Бакет упорядковує файли. Власних правил він не має, і до нього не причеплено жодного редактора policy. storage.objects це місце, де живуть правила, для всіх ваших бакетів одразу, а bucket_id це її колонка. Тож правило, яке каже "тільки в бакеті avatars", пишеться як умова на цій колонці.

Саме тому не допомогло й табличне правило, яке ви написали минулого тижня. Правило на profiles це правило про рядки в profiles, а файл це рядок у storage.objects.

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

Чи треба робити бакет публічним, щоб вивантаження працювало?

Ні. За бакетом стоять два різні питання, і перемикач відповідає тільки на одне. Хто може винести файл, вирішує публічність. Хто може покласти файл усередину, це тема вашої помилки, і документація Supabase однозначна: налаштування туди не дістає. Зі сторінки про два види бакетів, прочитано 11 жовтня 2026:

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

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

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

Що бакет насправді перевіряє при вивантаженні

Одне правило, і зветься воно insert.

Сторінка Supabase про контроль доступу прямо називає стан за замовчуванням: без policy Storage не дозволяє жодного вивантаження в бакет, а ви дозволяєте операції по одній, записуючи їх на storage.objects. Далі вона називає ту, яка вам потрібна:

Наприклад, єдина policy RLS, потрібна для вивантаження обʼєктів, це надати дозвіл INSERT на таблицю storage.objects.

У цього є друга половина, і вона найчастіша причина, чому правило insert не прибирає помилку:

Щоб дозволити перезапис файлів через функціональність upsert, вам доведеться додатково надати дозволи SELECT та UPDATE.

Якщо ваше вивантаження передає upsert: true, що просить Storage замінити файл із тим самим іменем, коли такий уже є, то правила insert самого по собі замало. Замінити файл це три питання замість одного: чи можна вам додати цей рядок, чи можна побачити той, що вже є, і чи можна його змінити.

Про що ваш застосунок просить StorageЩо йому потрібно на storage.objects
вивантажити новий файлправило insert
замінити файл із тим самим іменем (upsert)insert, а ще select і update
відкрити файл у приватному бакетіправило select
перелічити те, що лежить у бакетіправило select
видалити файлправило delete

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

Один склад, два отвори. Перемикач під'єднано до нижнього. Вивантаження приходить до верхнього, де правило, якого ще ніхто не написав, вирішує, чи пустити його.

Правило, що дає вивантажувати вашому застосунку, і тільки йому

Назвіть бакет і скажіть, для кого правило діє.

Відправна точка документації обмежує вивантаження одним бакетом і відвідувачами з входом:

create policy "Allow authenticated uploads"
on storage.objects
for insert
to authenticated
with check (bucket_id = 'my_bucket_id');

to authenticated це частина, яку не варто пропускати. Вона означає, що правило стосується відвідувачів, які увійшли; заберіть її, і правило стосується всіх, сторонніх зокрема.

Для всього, що належить конкретній людині, версія, яку публікує Supabase, кладе файли кожної людини в теку, названу її іменем:

create policy "Allow authenticated uploads"
on storage.objects
for insert
to authenticated
with check (
  bucket_id = 'my_bucket_id' and
  (storage.foldername(name))[1] = (select auth.jwt()->>'sub')
);

storage.foldername(name) розбиває шлях збереженого файлу на теки, тож [1] це перша з них. auth.jwt()->>'sub' це id того, хто увійшов і робить запит. Прочитані разом, ці два рядки кажуть, що ви можете покласти файл у теку, названу вашим іменем, у тому бакеті, і більше ніде.

Обидва ці приклади належать Supabase, а не нам, прочитано 11 жовтня 2026, із my_bucket_id там, де вони його лишили, щоб ви бачили, яка частина є іменем вашого власного бакета.

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

Що правило на читання віддає, хоч його й не просили

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

Сторінка Supabase про допоміжні функції каже це прямо: один привілей SQL на кшталт SELECT використовують кілька дій Storage. Сторінка про контроль доступу далі застерігає щодо конкретного випадку, під прикладом, який відкриває бакет з аватарами для всіх:

Фільтр allow_any_operation() тут критичний, бо без нього користувачі змогли б перелічити вміст бакета.

Саме цю прогалину ми вимірюємо ззовні. Ми просканували 8 435 живих застосунків, що називають проєкт Supabase. Перевірка бакетів дістала відповідь від 4 703 з них, і 792 з цих відповіли на наш запит переліку іменами того, що лежало всередині, без жодного входу. Це приблизно один із шести застосунків, які ми змогли запитати.

Часто досить і самого імені, бо бакет, який перелічує, позбавляє стороннього клопоту вгадувати імена файлів. invoice-2026-03-hannah.pdf каже, чий це файл, ще до того, як його хтось відкриє.

Виправлення, яке документує Supabase, полягає в тому, щоб сказати, для якої дії Storage діє правило:

create policy "Allow users to list their own objects"
on storage.objects
for select
to authenticated
using (
  storage.allow_only_operation('object.list')
  and owner_id = (select auth.uid()::text)
);

storage.allow_only_operation і сестринська storage.allow_any_operation це спосіб, у який правило select звужується до однієї з дій, що ділять привілей. Без котроїсь із них правило, яке ви написали, щоб застосунок міг показати картинку, це ще й правило, яке зачитає все, що є в бакеті.

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

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

Коли публічність це правильна відповідь

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

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

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

Як тримати бакет правильним після сьогодні

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

Reeve Monitor проганяє дев'ять зовнішніх перевірок за вас знову:

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

Reeve Care тримає копію того, що там лежить:

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

Обидва є на сторінці цін, яка іноді нижча за вказану тут і ніколи не вища.

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

Що робити

  • Читайте повідомлення як відмову. Файл не збережено, бакет незмінний, і відновлювати нічого не треба.
  • Додайте правило insert на storage.objects, яке називає ваш бакет у bucket_id і несе ту клаузу TO, яку ви мали на увазі.
  • Якщо ваше вивантаження передає upsert: true, додайте ще select і update, інакше саме лише правило insert помилку не прибере.
  • Поки це робите, лишіть перемикач публічності там, де він є. Щодо вивантажень він голосу не має, а щодо того, хто може відкрити вже збережене, має цілком.
  • Перечитайте кожне правило select на storage.objects і спитайте, що воно дозволяє, окрім завантаження, заради якого ви його написали. Перелік ділить цей привілей.

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

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

Чому моє вивантаження в Supabase падає з помилкою row-level security?

Бо Supabase Storage запитав правила, покладені на таблицю storage.objects, чи можна впустити ваш файл, і не знайшов жодного, яке сказало б так. Storage записує в цю таблицю один рядок на кожен файл, який тримає, і Row Level Security діє на цей рядок так само, як на рядок будь-якої іншої таблиці. Жодного файлу не збережено, і нічого з того, що вже лежало в бакеті, не змінилося. Повідомлення несе код Postgres 42501, той самий, що й при відмові зберегти рядок у вашій власній таблиці.

Чи треба робити бакет публічним, щоб вивантаження працювало?

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

Які policy потрібні бакету?

Вивантаженню потрібне одне правило insert на storage.objects. Якщо ваш код замінює файли з тим самим іменем, а саме це робить upsert, Supabase каже, що потрібні ще select і update. Відкрити файл у приватному бакеті потребує правила select, і перелічити вміст бакета теж, бо обидві ці дії Storage працюють на одному привілеї SQL. Видалення потребує правила delete. Писати всі чотири не обовʼязково, лише ті, які ваш застосунок справді виконує.

Чи небезпечний публічний бакет?

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

Як перевірити, чи мій бакет можна перелічити?

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

Автор

Vlad Tkachenko

Засновник Reeve

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

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

Читати далі

Усі статті

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

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

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

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