Основи безпеки
Які API-ключі безпечні у браузері, а які ні
Ваш ключ anon у Supabase створений, щоб бути публічним. Ключ service_role для цього не створений, і він ігнорує всі правила, які ви задали. Як їх розрізнити.
Коротко
- Знайти API-ключ у коді вашого застосунку ще не означає проблему. Деякі ключі там і мають бути.
- Публічні ключі безпечні у браузері, а секретні ні: секретний ключ у браузері працює як відкрита каса.
- Дві перевірки розділяють їх менш ніж за хвилину: прочитати префікс і, якщо ключ Supabase старий, розкодувати роль.
Якщо ви зібрали застосунок у Lovable, Bolt, v0, Cursor чи Replit, рано чи пізно станеться таке: хтось відкриє ваш сайт, натисне F12 і повідомить, що ваш API-ключ «у відкритому доступі». Для застосунку, код якого ви й самі не читаєте, це тривожна новина.
Ось що гайд за гайдом подає неправильно: частина цих ключів там і має бути. Ставитися до кожного видимого ключа як до витоку означає або панікувати без приводу, або, що значно гірше, навчитися ігнорувати попередження. І проігнорувати його того дня, коли воно справді важить.
Чи погано, що мій API-ключ видно?
Зазвичай ні. Усе залежить від того, який це ключ.
Кожен сервіс, що спілкується з браузером, видає два різні типи облікових даних. Перший з них служить адресою. Другий служить в'язкою ключів від будівлі. Обидва називаються «API-ключ», і саме звідси більша частина плутанини.
Адресу можна публікувати безпечно. Вона лише каже, до якого проєкту належить запит; перевірка прав відбувається деінде. В'язку ключів публікувати не можна, бо вона і є перевіркою прав: хто нею володіє, робить усе, що вона дозволяє, звідки завгодно.
Вашому застосунку потрібна адреса у браузері, щоб узагалі працювати. В'язка ключів там не потрібна ніколи.
Чому ваш застосунок узагалі надсилає ключі у браузер
Бо запит іде з браузера вашого відвідувача, а не з вашого сервера.
Коли ваш застосунок завантажує список замовлень ваших користувачів, цей запит іде напряму з браузера відвідувача до вашого постачальника бази даних. Він має вказати, до якого проєкту належить, і цей ідентифікатор має бути на сторінці, бо саме звідти виконується запит.
Немає варіанту, в якому цей ідентифікатор лишався б таємним. Він доходить до кожного відвідувача за задумом. Саме тому постачальники ділять облікові дані надвоє: вони знають, що одні з них стануть публічними, тож зробили їх нешкідливими.
Безпека береться не з приховування адреси. Вона береться з правил, які ви задаєте на іншому кінці: у випадку Supabase це Row Level Security, яка вирішує рядок за рядком, кому що видно. Саме ці правила варто перевіряти, а увімкнути їх не означає бути захищеним.
Дві родини ключів
| Постачальник | Ключ | Безпечний у браузері? | Що він робить |
|---|---|---|---|
| Supabase | sb_publishable_…, або anon у старіших проєктах | Тут доречно | Каже, про який проєкт ідеться. Кожен запит і далі фільтрується вашими правилами Row Level Security. |
| Supabase | sb_secret_…, або service_role у старіших проєктах | Ніколи | Повністю обходить Row Level Security. Читає і змінює кожен рядок у кожній таблиці, хоч би що казали ваші правила. |
| Stripe | pk_live_… (публічний) | Тут доречно | Створює форми оплати. Не може ані переказати гроші, ані прочитати клієнтів. |
| Stripe | sk_live_… (секретний) | Ніколи | Повний доступ до акаунта: списання, повернення, виплати, картки клієнтів. |
| OpenAI / Anthropic | будь-який ключ | Ніколи | Публічного варіанту не існує. Кожен ключ виставляє рахунок просто вам. |
Ця закономірність працює й за межами цих трьох. Якщо постачальник пропонує лише один тип ключа, вважайте його секретним: його місце на сервері.
Як розрізнити їх за 60 секунд
У Stripe і в більшості постачальників достатньо префікса. pk_ означає
публічний і безпечний. sk_ означає секретний, а отже ні. Деякі постачальники
ставлять посередині _test_ або _live_: витік тестового ключа створює значно
меншу проблему, ніж витік бойового, але замініть обидва.
У Supabase усе залежить від того, наскільки старий ваш проєкт. Supabase видавав ключі двох різних форматів, і обидва сьогодні працюють у живих застосунках.
Нові проєкти: читайте префікс, так само як у Stripe. sb_publishable_… має
жити у вашому застосунку. sb_secret_… – це той, який варто замінити
сьогодні. Розкодовувати нічого не треба: для чого ключ, написано спереду.
Що змінилося і що це означає для вашого застосунку,
якщо ви ще з ними не стикалися.
Старіші проєкти: доводиться зазирнути всередину ключа. Початкова пара,
anon і service_role, є JWT: три блоки незрозумілих символів, розділені
крапками, і середній блок містить читабельну інформацію, а не шифр. Там роль
ключа записана відкритим текстом.
В обох випадках інструмент не потрібен. Відкрийте Settings → API Keys в панелі Supabase, і там написано, який ключ який. Якщо ж вам зручніше перевірити той ключ, який ви справді знайшли у застосунку, середня частина старого ключа розкодовується приблизно в таке.
{
"iss": "supabase",
"ref": "abcdefghij…",
"role": "anon", ← ось це слово і має значення
"iat": 1750000000
}
"role": "anon" означає безпечний ключ. "role": "service_role" означає той, який варто
замінити сьогодні. У старому ключі це одне слово і є всією різницею, і саме тому
сканер, який просто шукає рядки, схожі на ключі, видає шум замість відповіді:
він не може сказати, який із двох однакових на вигляд ключів ви насправді
опублікували.
Якщо перебирати застосунок ключ за ключем не хочеться, наше безкоштовне сканування читає ваш живий сайт і каже, які з них видно ззовні. Це триває приблизно 20 секунд і не потребує акаунта: сканувати застосунок.
Що насправді стається, коли витікає секретний ключ
Кожен рядок вашої бази стає доступним для читання й запису тому, хто знайшов
ключ, включно з таблицями, які ви ніколи не відкривали застосунку, і з
персональними даними ваших користувачів. Row Level Security не поширюється на
секретний ключ Supabase (sb_secret_…, або service_role у старішому
проєкті). У цьому й полягає його призначення.
Шкода теж не теоретична. Зазвичай про неї дізнаються з повідомлення в підтримку про дані, які змінилися самі, або з таблиці, яка раптом виявилася порожньою.
Витік ключа sk_live_ у Stripe означає повернення, списання й картки клієнтів.
Витік ключа постачальника ШІ означає рахунок, інколи дуже великий, який
приходить раніше, ніж хтось це помітить.
Нічого з цього не потребує витонченого зловмисника. Автоматичні сканери обходять публічні сайти саме в пошуках таких рядків, і їм не треба знати, хто ви, щоб знайти ваш.
Якщо потрібна версія для конкретної платформи, у нас є зрозумілий розбір для застосунків на Supabase.
Що зробити просто зараз
Що робити
- Знайдіть кожен ключ у застосунку і визначте, який це. Префікс підходить для Stripe і для нових ключів Supabase; поле
roleвсередині підходить для старіших ключів Supabase. - Якщо знайшли секретний ключ у браузері, спершу замініть його. Видалити його з коду двері не зачиняє: старе значення лишається в історії версій і в кешованих копіях вашого сайту.
- Перенесіть на сервер те, для чого цей ключ був потрібен: edge-функцію, serverless-маршрут, будь-що, що не є браузером.
- Увімкніть Row Level Security для кожної таблиці, а потім перевірте, що вона справді працює. Публічний ключ (
sb_publishable_…абоanon) безпечний лише завдяки цим правилам; без них він читає всю вашу базу. - Перевірте рахунки та журнали після будь-якого розкриття секретного ключа. Заміна зупиняє те, що станеться далі, а не те, що вже сталося.
Якщо вам зручніше йти за списком, 10-хвилинний чекліст безпеки охоплює цей пункт і решту речей, які варто вимкнути у щойно запущеному застосунку.
А якщо витік колись спорожнить таблицю, чи повернете ви її, цілком залежить від того, що ви копіювали.
Поширені запитання
Мені сказали, що мій API-ключ у відкритому доступі. Це привід панікувати?
Не раніше, ніж ви дізнаєтеся, який це ключ. Якщо він починається з pk_ або це ключ anon у Supabase, він і має бути публічним, і все гаразд. Якщо він починається з sk_ або це ключ service_role, замініть його негайно, а потім подивіться, до чого він мав доступ.
Чи можна просто сховати ключ, щоб його ніхто не знайшов?
Ні. Усе, чим може скористатися ваш браузер, може прочитати й відвідувач: мініфікація, перейменування чи обфускація затримають когось на кілька секунд. Рішення ніколи не полягає в тому, щоб сховати секретний ключ у браузері: його треба перенести на сервер або використати публічний ключ, який від початку було безпечно показувати.
Чому Supabase дає мені ключ, який може прочитати будь-хто?
Тому що ваші дані захищає не ключ anon. Він лише каже, з яким проєктом ви говорите. Справжній захист дає Row Level Security, яка вирішує рядок за рядком, що саме дозволено бачити кожному відвідувачу. Саме тому ключ anon з увімкненою RLS цілком нормальний, а той самий ключ без RLS відкриває вашу базу даних.
Мій ключ Supabase починається з sb_publishable_. Це те саме, що ключ anon?
Він виконує ту саму роботу. Supabase перейменував свої ключі: sb_publishable_ заступив ключ anon, а sb_secret_ заступив ключ service_role. Тож ключ, що починається з sb_publishable_, і має жити у вашому застосунку, а той, що починається з sb_secret_, не має жити там ніколи. Старіші проєкти досі мають початкові ключі anon і service_role, і вони продовжують працювати; у панелі ви можете навіть бачити обидві пари поруч.
Я вже вставив секретний ключ у застосунок. Що тепер?
Спершу замініть його: у панелі постачальника створіть новий ключ і відкличте старий. Саме це справді зачиняє двері; прибрати його з коду недостатньо, бо старе значення лишається в історії версій і в кешованих копіях. Далі перенесіть на сервер те, для чого цей ключ був потрібен, і перевірте рахунки та журнали на те, чого ви не робили.