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

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

Supabase Row Level Security увімкнено. Таблиця досі публічна.

Увімкнути Supabase Row Level Security не означає захистити таблицю. Захищають політики, а та, що полагодила застосунок, часто впускає всіх.

Vlad Tkachenko7 хв читання

Коротко

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

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

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

Row Level Security увімкнено в Supabase. Чому таблиця досі публічна?

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

Уявіть перемикач як людину, яку ви поставили біля дверей. Перевести його не означає вирішити, хто пройде. Це означає, що тепер хтось звіряє список. Ваші політики і є той список. Порожній список не пускає нікого; список зі словом «усі» не спиняє нікого. І те, і те лишається Row Level Security в увімкненому стані, і ваша панель показує однакову зелень в обох випадках.

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

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

Тієї миті, коли ви вмикаєте Row Level Security, ваш застосунок перестає показувати дані, і те, що ви зробили далі, аби він знову запрацював, і є тим, на що варто подивитися.

Ця послідовність цілком звичайна, і саме тут усе йде не так. З увімкненим налаштуванням і без жодної написаної політики Postgres (рушій бази даних, на якому працює Supabase) за замовчуванням відхиляє кожен запит, тож ваші списки повертаються порожніми, а екрани лишаються білими. Щось має потрапити до списку. Якщо ви попросили Cursor чи Lovable це полагодити або вставили перший уривок, після якого помилка зникла, те, що ви маєте зараз, найпевніше має такий вигляд:

CREATE POLICY "Enable read access for all users"
  ON public.profiles
  FOR SELECT
  USING (true);

USING (true) задає умову, яку рядок має задовольнити, перш ніж база даних його віддасть. Умову true задовольняє кожен рядок. Там усередині є друга деталь, повз яку легко пройти: без клаузи TO політика діє для public, а public однаково охоплює й відвідувачів, які увійшли, і цілком сторонніх людей.

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

Це лагодить читання, і тільки читання. Якщо ваш застосунок ще й зберігає в цю таблицю, наступне, що ви зустрінете, це new row violates row-level security policy, тобто те саме налаштування, яке відмовляє в записі, і жодна політика читання цього не прибере.

Чотири стани, в яких може бути таблиця

Два з них безпечні, а два ні, і перемикач не каже вам, які саме. «Публічний ключ» нижче означає той, якому місце у вашому застосунку: sb_publishable_… у нових проєктах Supabase, anon у старіших.

Row Level SecurityПолітикаВаш застосунокСторонній з вашим публічним ключем Supabase
Вимкненобайдуже, жодну не читаютьПрацюєЧитає кожен рядок
Увімкненожодної не написаноЗламанийНе читає нічого
УвімкненоUSING (true)ПрацюєЧитає кожен рядок
УвімкненоUSING (auth.uid() = user_id)ПрацюєЧитає лише свої
Позначка під кожним стовпцем не повторює перемикач над ним. Два з них увімкнені, і один із цих двох віддає все.

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

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

«Authenticated» не означає те саме, що «ваш»

Політика, яка дозволяє authenticated, дозволяє всім, хто має акаунт, а це (якщо у вашому застосунку відкрита реєстрація) усі, хто готовий її заповнити.

Це тонший варіант тієї самої помилки, і він переживає чимало переглядів, бо має охайний вигляд. TO authenticated USING (true) читається як обмеження, і воно ним є: воно відсікає тих, хто ніколи не реєструвався. Чого воно не робить, так це не спиняє одного вашого клієнта від читання рядків іншого клієнта, а саме це ви зазвичай і мали на увазі під словом «приватне».

Правило, яке це робить, називає власника рядка:

CREATE POLICY "Users read their own rows"
  ON public.orders
  FOR SELECT
  TO authenticated
  USING (auth.uid() = user_id);

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

Як перевірити власні таблиці за дві хвилини

Відкрийте Supabase, зайдіть в Authentication → Policies і читайте вираз усередині кожної політики, а не значок біля кожної таблиці.

Три речі, які варто шукати:

  • Політику, умова якої true. Вирішуйте таблиця за таблицею, чи спокійно вам було б бачити ці дані на відкритій сторінці. Для переліку опублікованих статей відповідь буде «так», для будь-чого, де є людина, – «ні».
  • Політику без клаузи TO. Вона діє для всіх, хоч ви увійшли, хоч ні, навіть коли решта правила має дуже конкретний вигляд.
  • Таблицю з увімкненим налаштуванням, без жодної політики, і застосунок, який попри це працює. Це поєднання означає, що до ваших даних дістається щось іншим шляхом, і звичним поясненням є секретний ключ: sb_secret_…, або service_role у старішому проєкті. Він ігнорує кожну написану вами політику. Які ключі API безпечні у фронтенді розповідає, як відрізнити його від безпечного.

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

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

Який вигляд це має, поки триває

Ніякого. Саме такої форми ця річ, і саме тому вона лишається на місці місяцями.

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

Коли це випливає, то випливає збоку. Клієнтка питає, звідки хтось знав те, що знав лише ваш застосунок. Список адрес, який ви ніколи не публікували, десь виринає. А якщо щедре правило охоплює і запис, FOR ALL замість FOR SELECT, то будь-хто може ще й змінювати та видаляти рядки. Цей варіант люди помічають як таблицю, що раптом стала порожньою.

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

Що зробити цього тижня

Що робити

  • Відкрийте Authentication → Policies у Supabase і прочитайте умову кожної політики, таблиця за таблицею. Значок на таблиці не є відповіддю.
  • Для кожної таблиці, де є люди (користувачі, профілі, замовлення, повідомлення), перевірте, що правило називає власника рядка, а не дозволяє всім.
  • Замініть кожне USING (true) на цих таблицях правилом, яке порівнює auth.uid() зі стовпцем власника, і після цього переконайтеся, що застосунок ще завантажується.
  • Додайте клаузу TO, яку ви мали на увазі. Політика без неї діє на сторонніх так само, як на відвідувачів, що увійшли.
  • Якщо в таблиці налаштування увімкнено, політик немає, а застосунок усе одно показує її дані, з'ясуйте, що це обходить, перш ніж чіпати щось інше.

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

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

Я увімкнув Row Level Security, і застосунок перестав показувати дані. Я щось зламав?

Ні, це налаштування працює так, як має. Коли Row Level Security увімкнено, а політик не написано, Postgres (рушій бази даних під Supabase) за замовчуванням відхиляє кожен запит, зокрема й запити вашого власного застосунку. Розв'язання полягає в тому, щоб додати політику, яка описує, хто які рядки має бачити. Помилка, якої варто уникнути: додати таку, що дозволяє всім, бо саме вона повертає застосунок до життя і лишає таблицю відкритою.

Чи буває USING (true) правильною політикою?

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

Чи захистить мене Row Level Security, якщо витік секретний ключ?

Ні. Секретний ключ Supabase (sb_secret_ у нових проєктах, service_role у старіших) повністю обходить Row Level Security, саме для цього він і існує. Кожну написану вами політику буде пропущено, на кожній таблиці. Якщо цей ключ у вашому фронтенді, ваші політики нічого для вас не роблять, і перевипуск ключа стає першим завданням, ще до будь-якої роботи над правилами.

Чи потрібен Row Level Security, якщо в застосунку вже є екран входу?

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

Автор

Vlad Tkachenko

Засновник Reeve

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

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

Читати далі

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

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

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

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