Основи безпеки
"No API key found in request" у Supabase, і хибне виправлення
"No API key found in request" означає, що запит дійшов до Supabase без ключа. Більшість відповідей натомість вказують на правила вашої бази.

Коротко
- "No API key found in request" означає, що ваш запит дійшов до Supabase без заголовка apikey, тож його розвернули на вході ще до того, як щось запитали у вашої бази даних.
- Правильне виправлення це ваш публічний ключ у тому самому заголовку, тобто ключ, який і має бути відкритим. Клієнтська бібліотека Supabase надсилає його за вас у кожному запиті.
- Два виправлення, до яких беруться натомість, це секретний ключ і послаблене правило на таблиці. Обидва вимикають повідомлення, і обидва лишають ваші рядки доступними кожному, хто має ключ, що їде всередині вашого застосунку.
Учора ваш застосунок працював. Сьогодні список, який зазвичай наповнювався рядками, лишається порожнім, а якщо відкрити консоль браузера, там замість даних стоїть коротка відповідь від Supabase:
{
"message": "No API key found in request",
"hint": "No `apikey` request header or url param was found."
}
Це все, що ви отримуєте. Тут не сказано, яку таблицю ви читали, що надіслали й що змінювати.
Ось те, що посібник за посібником переказує неправильно: "No API key found in request" про заголовок, якого бракує, а більшість порад, які ви знайдете, про правила вашої бази даних. На форумі обговорень Supabase це повідомлення має власну гілку, шістнадцять людей пропонують там вісім різних засобів, і троє з них радять додати або послабити правило на одній із ваших таблиць. Усі, хто спробував, кажуть, що повідомлення припинилося. Чи було правило колись причиною це інше питання, і гілка його ніколи не закриває. Напевно відомо лише те, що після цього таблицю може читати більше людей, ніж раніше.
Що означає "No API key found in request"
Supabase розвернув ваш запит на вході, перш ніж у вашої бази даних щось запитали.
Кожен запит до вашого проєкту спершу приходить до брами, яку Supabase ставить
перед усім іншим. Її робота в ту мить вузька: прочитати заголовок apikey,
звірити значення з ключами вашого проєкту й пропустити запит далі. Коли читати
нічого, вона зупиняється там і відповідає 401, кодом стану для «я не знаю, хто
ви». Довідка Supabase про цю браму сама каже, що відсутній або недійсний ключ
відхиляють саме так.
Уявіть браму як вахтера біля вхідних дверей, а не як замок на шафі з документами. Вахтер просить перепустку в кожного, хто приходить. Правила, які вирішують, кому яку шафу відчиняти, лежать двома поверхами вище, і тут до них ніхто не звертався, бо далі входу ніщо не пройшло.
Як на помилку, ця лагідна. Нічого не прочитано, нічого не записано, і ваші дані точно такі, якими були. Один запит відхилено.
Чому мій запит прийшов без ключа?
Бо значення, яке ваш застосунок мав надіслати, не потрапило до запиту, який браузер зробив насправді. Чотири причини покривають майже всі випадки.
Ключ так і не дійшов до зібраного застосунку. Ваш код читає його зі змінної
середовища, у збірці, з якої вийшов живий сайт, цієї змінної не було, і
createClient отримав порожній рядок. Усе компілюється, застосунок
завантажується, і кожен запит іде без ключа. У проєкті на Vite чи Next змінна ще
й має нести префікс, який позначає її безпечною для браузера, і це
окрема пастка.
Ви звертаєтеся до REST-шляху вручну. fetch, написаний на
/rest/v1/ваша_таблиця, надсилає ті заголовки, які ви набрали, і більше нічого.
Клієнтська бібліотека Supabase додає apikey за вас; запит, написаний руками,
має лише те, що ви йому дали.
Щось посередині його прибрало. Правило перезапису, проксі або ваш власний шлюз стоїть між застосунком і Supabase і передає запит без заголовка.
Ви дивитеся на перенаправлення, а не на запит даних. Якщо повідомлення
з'являється після того, як хтось увійшов або натиснув лист підтвердження, а в
адресному рядку досі ваша адреса Supabase, дивитися треба налаштування Site URL
у розділі Auth. Відповідь із найбільшою кількістю реакцій у всій тій гілці
Supabase належить людині, яка записала там адресу свого сайту без https://
попереду.
Виправлення, в один рядок
Надсилайте свій публічний ключ у заголовку apikey.
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
'https://ваш-проєкт.supabase.co',
'sb_publishable_…',
)
Створіть клієнт отак, і бібліотека покладе ключ у потрібний заголовок у кожному
запиті, тож заголовок ви не пишете взагалі. Довідка Supabase точно каже, який це
заголовок: публічні та секретні ключі їдуть у apikey, а не в
Authorization: Bearer, бо це короткі непрозорі рядки, і все, що намагається
перевірити їх як JWT, зазнає невдачі.
Цей ключ і має бути у вашому застосунку. Він каже, якому проєкту належить запит, а те, до чого відвідувач із ним дістанеться, вирішують правила на ваших таблицях, і саме тому ключ може бути відкритим. Які ключі API безпечні у фронтенді проходить решту родини.
Виправлення, яке робить гірше
Секретний ключ підходить до того самого заголовка, і в старішому проєкті він вимкне повідомлення й працюватиме далі.
Це один невлучний клік. Ваша панель показує обидва ключі на одній сторінці, і стара пара схожа: та сама форма, та сама довжина, один під одним. Наскільки поганий цей клік, залежить від того, яку пару видає ваш проєкт.
Новіший секретний ключ у браузері відхиляють. Supabase блокує
sb_secret_… за заголовком User-Agent і відповідає 401. Тож вставити його у
фронтенд помилку не прибирає, і це найкращий можливий тут результат.
Старий ключ service_role такого блокування не має. Він працює. Список
наповнюється, застосунок поводиться точно як минулого тижня, а ключ тепер лежить
у файлі, який може завантажити будь-який відвідувач. Там він ігнорує кожне
правило, яке ви написали: service_role несе атрибут Postgres BYPASSRLS, тож
політика до нього не застосовується ніколи. Кожен рядок у кожній таблиці,
доступний на читання й запис тому, хто відкриє той файл.
Примітку Supabase про блокування в браузері варто прочитати двічі. Блокування відповідає 401, а зловмисник усе одно може скористатися ключем з інших інструментів. Захищені тут запити вашого власного застосунку; той самий ключ у тому самому файлі відповідає на запит, надісланий зі скрипта.
Інший хибний поворот, і чому він такий легкий
Послабити правило на таблиці теж вимикає повідомлення, і це змінює, хто може читати ваші рядки.
Ось та гілка Supabase цілком, згрупована за тим, що кожна відповідь пропонує змінити:
| Що змінити | Скільки з восьми відповідей | Що це змінює насправді |
|---|---|---|
| Site URL у розділі Auth | 1 | Куди веде перенаправлення |
| Політику або grant | 3 | Хто читає й пише ваші рядки |
| Сам запит або сесію | 3 | Ваш власний код |
Заголовок apikey | 1 | Те, про що просила брама |
Останній рядок це той, що відповідає на підказку в повідомленні. Це найнижчий коментар у гілці, і він має один голос.
Жодна з решти семи не написана зі злого наміру, і саме це робить справу складною. Одне повідомлення справді з'являється в кількох ситуаціях, людина, яка відповідає, справді повернула свій застосунок до життя, а політика, якої й так бракувало, справді потребує лагодження. Збивається порядок: правило переписують, щоб вимкнути повідомлення про заголовок, і саме правило та половина цієї пари, яку потім ніхто не перевіряє. Ваш застосунок працює в обох випадках, тож ніщо не скаже вам, що саме з двох ви зробили.
Ззовні результат видно. Наш сканер читає живі застосунки так, як це зробив би незнайомець, і з 31 056 застосунків, яким він виставив оцінку, три опублікували секретний ключ Supabase. Це робить таку знахідку найрідкіснішою з усіх, що ми маємо, і найсерйознішою.
Сусідній порив куди поширеніший. За 8 435 із тих застосунків ми знайшли проєкт Supabase. У 3 680 із них питання «чи може незнайомець прочитати цю таблицю» взагалі дістало придатну відповідь, і 2 096 відповіли так: щонайменше одна таблиця віддала рядки запиту ззовні, у якому не було жодного облікового запису. У 394 з них відкрита таблиця мала назву таблиці з людьми.
Частина цього зроблена свідомо. Опубліковане меню, сторінка оголошень і публічний журнал змін живуть у таблицях, які незнайомець і має читати, а наш сканер не вміє відрізнити їх від таблиці клієнтів, відкритої через недогляд. Він повідомляє, що відповіло, і лишає судження вам, і саме тому увімкнути Row Level Security це не те саме, що бути захищеним.
Якщо ви радше побачите власну відповідь, ніж виводитимете її, наше безкоштовне сканування читає ваш живий сайт і каже, до чого воно дістається ззовні. Воно триває близько 20 секунд і не потребує облікового запису: просканувати застосунок.
Як зрозуміти, який ключ ви вставили
Прочитайте початок рядка.
sb_publishable_… належить вашому застосунку. sb_secret_… ніколи.
Розкодовувати нічого не треба, бо призначення ключа написане в нього спереду.
Довге значення, що починається з eyJ, належить старішій парі, і саме там ці
два стає важко розрізнити. Середня секція такого ключа це читабельна інформація,
а не шифр, і вона несе поле, яке закриває питання:
{
"iss": "supabase",
"role": "anon", ← ось що важить
"iat": 1750000000
}
anon це публічний із пари. service_role це той, який варто замінити сьогодні.
Документація Supabase каже про це сухіше, ніж сказали б ми: якщо туторіал або
асистент зі штучним інтелектом радить скопіювати довгий ключ, що починається з
eyJ, його писали для старих ключів.
Обидві пари можуть бути чинними одночасно, і саме на цій деталі спотикаються.
Створення публічного або секретного ключа лишає ваші ключі anon та
service_role рівно там, де вони були, і вони працюють, доки ви не вимкнете їх у
панелі окремим кроком.
Що змінилося з новішими ключами розповідає про цей
перехід, а
де у вашій панелі лежить кожне значення
це сторінка, яку варто відкрити, якщо ви там ніколи не були.
Якщо секретний ключ уже у вашому застосунку
Замініть його, перш ніж щось редагувати, і працюйте в тому порядку, який дає Supabase.
Створіть у панелі новий секретний ключ. Замініть старий усюди, де проєкт ним
користується. Переконайтеся, що кожна частина застосунку перейшла на новий. І
лише тоді виводьте старий з ужитку, причому останній крок різниться залежно від
пари: видалення секретного ключа скасувати неможливо, а вимкнення старої пари
anon і service_role оборотне, тож ви зможете увімкнути їх назад, якщо
пропустили якийсь клієнт.
Чи міняти ключ першим, чи першим латати витік, залежить від того, чи копія вже публічна. Заміна ключа service_role у Supabase проходить обидва порядки і каже, що потім читати в журналах.
Щоб так і лишилося, коли помилка зникла
Виправлення тримається до наступного промпту чи зміни, що зачіпає ваше налаштування Supabase. Одного промпту досить, щоб ключ знову опинився в застосунку або правило знову послабили, а застосунок працюватиме в обох випадках, тож зміну помітять, лише коли хтось подивиться.
Reeve Monitor дивиться за вас:
- усі дев'ять перевірок щогодини, для трьох застосунків максимум
- лист того дня, коли повторне сканування оцінить ваш застосунок нижче, ніж попереднє
- чи працює застосунок, кожні 60 секунд
- місячний звіт про те, що він побачив
Ключ, який може записати будь-який рядок, може й спорожнити будь-яку таблицю. Reeve Care зберігає копію вашої бази Supabase там, куди не дотягнеться жоден ключ із вашого застосунку.
- зашифрована копія щодня, поза вашим обліковим записом Supabase
- кожна копія перевірена, перш ніж її зарахувати, з підрахунком рядків у кожній таблиці
- відновлення в один клік, яке спершу зберігає поточний стан, а тоді щось замінює
- ваші завантажені файли теж, щойно ви під'єднаєте облікові дані Storage
- усе, що робить Monitor
Що зробити сьогодні
Що робити
- Читайте повідомлення як відмову на вході. Ваших рядків ніхто не чіпав, і відновлювати нічого не треба.
- Подивіться у вкладці мережі браузера на невдалий запит і перевірте, чи є там заголовок
apikeyузагалі. Один цей погляд скаже, це проблема ключа чи перенаправлення. - Створюйте клієнт Supabase із публічним ключем і дайте бібліотеці надсилати заголовок.
sb_publishable_…у новішому проєкті, ключanonу старішому. - Якщо секретний ключ уже у застосунку, спершу замініть його. Видалення з коду дверей не зачиняє, бо стара версія лишається в історії версій і в будь-якій кешованій копії вашого сайту.
- Поверніть на місце кожне правило, яке ви послабили в цих пошуках, і перевіряйте його ззовні, а не з попереднього перегляду свого конструктора.
Почніть із тієї однієї таблиці, з якої прийшла помилка, а далі прочитайте політики кожної таблиці, якої торкалися того самого тижня. Чекліст безпеки на 10 хвилин покриває, що ще зазвичай лишається відкритим у щойно запущеному застосунку, а посібник з безпеки Supabase проходить решту того, до чого може дістатися незнайомець.
Поширені запитання
Що означає "No API key found in request"?
Це означає, що ваш запит дійшов до Supabase без заголовка apikey, тож брама, яку Supabase ставить перед вашим проєктом, розвернула його ще до того, як база даних узяла участь. Нічого не прочитано, нічого не записано, і у ваших даних нічого не змінилося. Відповідь несе підказку, яка каже те саме іншими словами: no apikey request header or url param was found.
Який ключ Supabase іде в заголовок apikey?
Ваш публічний ключ, тобто sb_publishable_ у новішому проєкті та ключ anon у старішому. Supabase видає його саме для цього й очікує, що він буде видимим усередині вашого застосунку. Клієнтська бібліотека ставить його в заголовок у кожному запиті, тож якщо ви створюєте клієнт із публічним ключем, заголовок вручну не пишете ніколи.
Чи безпечно тримати ключ anon у браузері?
Так, і він має там бути, щоб ваш застосунок узагалі міг говорити з вашим проєктом. Ключ anon лише каже, якому проєкту належить запит; те, до чого відвідувач із ним реально дістанеться, вирішують правила Row Level Security на ваших таблицях. Які ключі API безпечні у фронтенді проходить усю родину ключів і показує, як їх читати.
Я скористався ключем service_role, і воно запрацювало. Це проблема?
Так, і саме це варто владнати сьогодні. Ключ service_role несе атрибут Postgres BYPASSRLS, тож ваші політики до нього не застосовуються ніколи. Опублікований усередині застосунку, він лежить у файлі, який може завантажити будь-який відвідувач, і той, хто завантажить, зможе читати й писати кожен рядок у кожній таблиці. Спершу змініть ключ, а потім перенесіть на сервер те, що його потребувало.
Як дізнатися, який ключ я вставив?
Прочитайте початок рядка. sb_publishable_ належить вашому застосунку, а sb_secret_ ніколи. Довге значення, що починається з eyJ, належить старішій парі, а ці два виглядають однаково, тож треба зазирнути всередину: середня секція розкодовується у читабельні дані з полем role, де стоїть або anon, або service_role.