Основи безпеки
Supabase security checker: п’ять перевірок, які зробите самі
Supabase security checker читає ваш опублікований застосунок, а не налаштування проєкту. Ось п’ять перевірок і як виконати кожну самостійно.

Коротко
- Supabase security checker читає ваш робочий застосунок так, як його читає незнайомець: бандл, який ви віддаєте, відповіді ваших таблиць, ваші бакети сховища, ваші заголовки.
- Власний Security Advisor від Supabase читає другу половину, тобто конфігурацію вашого проєкту. Жоден із двох не бачить того, що бачить інший.
- П’ять перевірок покривають погляд ззовні. Кожна запускається з термінала, не потребує облікового запису, а разом вони займають близько десяти хвилин.
Ви шукали Supabase security checker і отримали дев’ять штук. Один хоче ваш репозиторій. Другий хоче розширення для браузера. Третій хоче доступ на читання до всього вашого облікового запису Supabase, що досить близько до тієї самої речі, яка вас непокоїла, аби ви завагалися.
Ось те, чого жодна з тих сторінок не каже вголос. Перевірок п’ять, і кожну ви можете зробити самі, без облікового запису, без встановлення і без жодних даних доступу, які було б небезпечно віддавати. Десять хвилин у терміналі покривають те саме, що й ті інструменти.
П’ять нижче це ті, які наш власний сканер виконує проти проєкту Supabase, виписані як команди. Якщо ви будували з Lovable, Bolt, v0, Cursor, Replit, Windsurf чи Base44 і ваш застосунок узагалі щось зберігає, база за ним дуже ймовірно Supabase, і ось питання, які поставив би їй незнайомець.
Що насправді перевіряє Supabase security checker?
Ваш опублікований застосунок. Налаштування всередині проєкту це робота іншого інструмента, і Supabase той інструмент уже дає.
Security Advisor у вашій панелі читає власний каталог проєкту: у яких таблиць вимкнена Row Level Security, у яких функцій нестрогий search path, які розширення лежать у схемі public. Він читає те, що ви налаштували.
Чекер читає те, що ваш застосунок віддає незнайомцю. Бандл JavaScript, який завантажують відвідувачі, відповідь таблиці на запит без входу, вміст бакета сховища, заголовки ваших сторінок. Вашої конфігурації він не бачить зовсім, і йому це не потрібно, бо він дивиться на її наслідок.
Дві половини розходяться частіше, ніж припускають назви. Таблиця може пройти Advisor із увімкненим перемикачем і написаною політикою й усе одно віддавати свої рядки будь-кому, бо написана політика впускає всіх. Це варте окремого читання, якщо ви з цим не стикалися: перемикач це не захист.
Перевірка 1: який ключ Supabase віддає ваш застосунок?
Відкрийте застосунок у браузері, натисніть F12 і подивіться на один запит.
Перейдіть на вкладку Мережа, перезавантажте сторінку й наберіть supabase у
полі фільтра. Ваш застосунок зробить щонайменше один запит на адресу, що
закінчується на .supabase.co. Клацніть його. Там усередині лежать дві речі,
потрібні вам для решти статті:
- Адреса запиту починається з URL вашого проєкту, щось на кшталт
https://abcdefghij.supabase.co. Перевірки 2 і 3 націлені на неї. - Заголовок запиту
apikeyнесе ключ, який ваш застосунок віддає кожному відвідувачу.
Скопіюйте обидві речі кудись, а тоді прочитайте сам ключ. Той, що починається з
sb_publishable_, належить вашому застосунку, і тут нема чого лагодити. Той, що
починається з sb_secret_, не належить ніколи, і знайти його означає закінчити
список достроково: змініть його сьогодні, поперед усього іншого на цій
сторінці. Старіші проєкти несуть пару без читабельного префікса, anon і
service_role, і розрізнити їх означає
прочитати роль усередині ключа.
Останній випадок трапляється рідко. Серед 30 998 робочих застосунків, які ми просканували в серпні 2026 року, секретний ключ Supabase у браузері трапився тричі.
Поки вкладка Мережа відкрита, випишіть назви таблиць, які видно в тих адресах запитів. Наступній перевірці вони потрібні.
Перевірка 2: чи відповідають ваші таблиці незнайомцю?
Спитайте базу, скільки рядків вона віддала б комусь без входу, і прочитайте число з відповіді.
curl -s -i \
-H "apikey: ВАШ_ПУБЛІЧНИЙ_КЛЮЧ" \
-H "Prefer: count=exact" \
-H "Range: 0-0" \
"https://ВАШ_ПРОЄКТ.supabase.co/rest/v1/profiles?select=count" \
| grep -i content-range
Підставте ключ та адресу проєкту з перевірки 1, а замість profiles напишіть
власну назву таблиці. curl уже встановлений на macOS, на Linux і на сучасному
Windows, тож нічого завантажувати наперед не треба.
Цей запит не забирає жодного рядка. select=count просить кількість,
Range: 0-0 не просить самих рядків, і вся відповідь приходить в одному
заголовку. Це той самий запит, який робить наш сканер, і саме тому він може
працювати проти чужого робочого застосунку, не торкаючись чужих даних.
Повернутися може чотири речі, і лише одна з них є знахідкою:
| Що друкується | Що це означає |
|---|---|
content-range: */0 | Без входу не читається нічого. Ця таблиця робить свою роботу. |
content-range: 0-0/128 | 128 рядків доступні кожному, хто має ключ із вашої сторінки. |
401 або 403 | Ключ відхилили ще до таблиці. Без відповіді, а не чисто. |
404 | Таблиці з такою назвою немає. Або ви вгадували, або вона зветься інакше. |
Відмова це той рядок, з яким слід бути обережним. 401 каже, що запит спинився,
не дійшовши до таблиці, а це не говорить вам нічого про те, чи таблиця
захищена, і з чотирьох саме його найлегше округлити до галочки.
Виконайте команду для кожної таблиці, яку ваш застосунок назвав у перевірці 1, а
тоді для звичайних, які застосунок зазвичай має: users, profiles,
customers, orders, messages, invoices. У цих шести лежать дані інших
людей.
Це перевірка, яка знаходить найбільше. Із 3 680 застосунків на Supabase, де ми змогли її завершити, 2 096 мали щонайменше одну таблицю, що відповідала на запит без входу, а в 394 із них відкрита таблиця мала назву про людей. Повний вимір і частка від чого це є окремою статтею.
Перевірка 3: чи перелічує бакет сховища власні файли?
Попросіть у бакета його вміст і подивіться, чи щось повернеться.
curl -s -X POST \
-H "apikey: ВАШ_ПУБЛІЧНИЙ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"prefix":"","limit":1}' \
"https://ВАШ_ПРОЄКТ.supabase.co/storage/v1/object/list/avatars"
Надішліть тіло запиту. POST на цю адресу без нічого всередині повертає "Body cannot be empty" для кожного бакета в кожному проєкті, хоч би як він був налаштований. Ми це знаємо, бо наша власна перевірка бакетів вийшла саме такою і місяцями бадьоро повідомляла, що нічого не перелічується.
Дві форми відповіді:
[]означає, що бакет закритий або що бакета з такою назвою немає. Ззовні ви не скажете, що саме, тож порожній перелік не є доказом нічого.- Масив із файлом усередині означає, що незнайомець може попросити у вашого проєкту каталог того бакета й отримати його.
Спробуйте назви бакетів, які використовує ваш застосунок, а тоді поширені:
avatars, public, uploads, files, images, documents.
Перелічуваність і публічність це два різні налаштування у двох різних місцях, і різниця вирішує, наскільки важливий публічний бакет. Перелік це те, що варто закрити, бо він позбавляє незнайомця клопоту вгадувати назву файлу. Із 27 269 застосунків, де ми змогли спитати, 792 відповіли переліком.
Перевірка 4: чи публікується ваш вихідний код поряд із застосунком?
Прочитайте останній рядок свого бандла JavaScript.
Поверніться на вкладку Мережа з перевірки 1, знайдіть найбільший файл .js,
який завантажив ваш застосунок, і скопіюйте його адресу. Тоді:
curl -s "https://ваш-застосунок.example/assets/index-abc123.js" | tail -c 120
Останній рядок із //# sourceMappingURL=index-abc123.js.map означає, що ваша
збірка записала source map і сказала браузерам, де вона лежить. Чи опублікувала
вона її насправді, з’ясовує ще один запит:
curl -s -o /dev/null -w "%{http_code}\n" \
"https://ваш-застосунок.example/assets/index-abc123.js.map"
200 означає, що завантажити її може будь-хто. Source map перетворює стиснений
JavaScript назад на ваші оригінальні файли, зі структурою тек, коментарями й
логікою. Вона відкриває ваш код, а будь-який ключ, який вона показує, і так уже
лежав поруч у бандлі, про що й була перевірка 1. 3 885 із 30 987 застосунків,
які ми змогли перевірити, публікують свою, і в деяких білдерів це типове
налаштування платформи, а не чиєсь рішення:
що показує опублікована source map.
Перевірка 5: що ваш хостинг надсилає з кожною сторінкою
Один запит на адресу вашого застосунку, і читаємо, що повернулося разом із ним.
curl -s -i "https://ваш-застосунок.example" | grep -i -E \
"content-security-policy|strict-transport-security|x-frame-options|x-content-type-options|referrer-policy"
П’ять заголовків, і ті, що не надрукувалися, це ті, яких вам бракує. Вони кажуть браузеру відмовити сторінці, якщо її завантажують усередині чужого сайту, наступного разу вимагати зашифроване з’єднання й перестати вгадувати, який тип файлу він щойно отримав.
Майже кожен застосунок провалює цю перевірку, і майже ніхто не може на неї вплинути. У 30 756 із 30 981 застосунку, який ми змогли перевірити, бракувало щонайменше одного, бо ці заголовки надсилає платформа хостингу, а власник на піддомені білдера не має де їх змінити. Ми це рахуємо й обмежуємо середнім рівнем: що передвіщають і чого не передвіщають відсутні заголовки.
Як читати те, що ви знайшли
За серйозністю, і це не той порядок, у якому ви їх виконували.
Секретний ключ у бандлі йде першим. Він пропускає кожне правило, яке ви написали на кожній таблиці, тож змініть його, перш ніж дивитися на будь-що інше тут.
Таблиця з людьми, що відповіла на перевірку 2, іде наступною. У users,
profiles, orders і messages лежать дані інших людей, і вони доступні прямо
зараз. Таблиця products, що відповідає так само, може бути цілком правильною, і
ви єдина людина, яка розрізнить ці два випадки.
Далі перелічуваний бакет, далі мапи й заголовки, які зазвичай є типовими налаштуваннями вашої платформи, а не чимось, що ви обрали.
Одна річ із усіх п’яти. Перевірка, яка не отримала відповіді, не пройшла.401, запит, у якого вичерпався час, таблиця, назву якої ви вгадали
неправильно: кожне з цього є питанням, що досі відкрите. Наш сканер пише
«Не вдалося перевірити» в таких рядках і не чіпає оцінку, а робити це вручну
означає вести той список самому.
Усе це також описує застосунок, який був у роботі в ту мить, коли ви спитали. Row Level Security вимикають, щоб сторінка завантажилася, бакет відкривають заради одного завантаження, ключ вставляють, щоб функція вийшла сьогодні ввечері.
Що зробити цього тижня
Що робити
- Виконайте перевірку 2 проти кожної таблиці, яку називає ваш застосунок, а тоді проти
users,profiles,customers,ordersіmessages. Саме там знахідки. - Якщо перевірка 1 виявила секретний ключ, спершу змініть його. Видалення з коду лишає старе значення робочим для тих, хто вже його має.
- Випишіть кожну пробу, що отримала відмову або взагалі не отримала відповіді. Вони лишилися без відповіді, а питання без відповіді це не чистий результат.
- Не чіпайте таблиці, які й мають бути публічними. Список товарів, що відповідає незнайомцю, це ваш застосунок, який працює правильно.
- Виконайте всі п’ять знову після деплою і після того, як хтось змінить правило бази. Жодна з цих подій не оголошує про себе.
Наш безкоштовний сканер безпеки виконує ці п’ять і ще чотири перевірки на будь-якій робочій адресі приблизно за двадцять секунд, без облікового запису й без переданих даних доступу. Він читає, ніколи не пише, а там, де не отримує відповіді, пише про це в рядку.
Усі п’ять ви можете зробити самі сьогодні. Чого не скаже вам жоден окремий прохід, так це чи будуть відповіді тими самими наступного місяця, і саме для цього ми зробили Reeve Care: він повторює ці перевірки за розкладом, пише вам листа, коли відповідь погіршується, і тримає перевірені резервні копії вашої бази Supabase, а також файли ваших користувачів, щойно ви під’єднаєте доступ до сховища. Що він стежить і скільки коштує.
Якщо термінал це не те місце, де вам хочеться бути, покрокова інструкція простою мовою для застосунків на Supabase пояснює, для чого кожне з цих налаштувань і де знайти його в панелі.
Поширені запитання
Чи знаходить Supabase Security Advisor усе?
У своїй половині він знаходить усе. Advisor читає конфігурацію вашого проєкту й повідомляє про таблиці з вимкненою Row Level Security, функції з нестрогим search path та подібні налаштування, якими ви керуєте з панелі. Чого він не бачить, так це вашого опублікованого застосунку: який ключ опинився в JavaScript, що завантажують ваші відвідувачі, чи справді написана вами політика зупиняє анонімний запит, і які заголовки надсилає ваш хостинг. Запустіть і Advisor, і п’ять перевірок із цієї статті, бо вони дивляться на різні речі.
Чи безпечно запускати ці перевірки проти власного проєкту?
Так. Кожна команда тут читає, і жодна не пише. Перевірка таблиць просить у бази кількість рядків і читає число із заголовка відповіді, тож жоден рядок ніколи не завантажується. Перевірка бакетів просить перелік і на цьому спиняється, не завантажуючи жодного файлу. Останні дві є звичайним запитом сторінки, таким самим, які ваші відвідувачі роблять цілий день. Запускати їх проти чужого проєкту це вже інше питання, і відповідь на нього одна: спершу спитати власника.
Мій ключ anon лежить у бандлі. Це проблема?
Ні. Ключ anon у старіших проєктах і sb_publishable_ у новіших зроблені саме для того, щоб бути в коді, який завантажують ваші відвідувачі. Він називає ваш проєкт і сам собою не дає нічого; те, які рядки поверне запит, вирішує Row Level Security. Ключ, якого там не має бути ніколи, це секретний: service_role у старіших проєктах і sb_secret_ у новіших, бо він пропускає кожне правило, яке ви написали.
Що означає content-range: */0?
Це ваша база каже, що видасть цьому запитувачу нуль рядків із тієї таблиці. Зірочка означає, що у відповіді немає жодного рядка, а число після скісної риски це кількість, яку запитувачу дозволено бачити. Тож */0 це відповідь, якої ви хочете від таблиці з персональними даними, а 0-0/128 означає, що 128 рядків доступні кожному, хто має ключ із вашого застосунку.
Чи потрібен мені ключ service_role, щоб це перевірити?
Ні, і чекер, який його просить, просить не те. Кожна перевірка тут використовує публічний ключ, який і так лежить у вашому застосунку, бо саме такий був би в незнайомця. Перевірка секретним ключем скаже вам, до чого дістанеться адміністратор, а це ніколи не було під питанням. Не вставляйте ключ service_role чи sb_secret_ у жоден сканер, наш зокрема: ніщо з того, що ми робимо, його не потребує.