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

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

Які API-ключі безпечні у браузері, а які ні

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

Vlad Tkachenko6 хв читання

Коротко

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

Якщо ви зібрали застосунок у Lovable, Bolt, v0, Cursor чи Replit, рано чи пізно станеться таке: хтось відкриє ваш сайт, натисне F12 і повідомить, що ваш API-ключ «у відкритому доступі». Для застосунку, код якого ви й самі не читаєте, це тривожна новина.

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

Чи погано, що мій API-ключ видно?

Зазвичай ні. Усе залежить від того, який це ключ.

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

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

Вашому застосунку потрібна адреса у браузері, щоб узагалі працювати. В'язка ключів там не потрібна ніколи.

Чому ваш застосунок узагалі надсилає ключі у браузер

Бо запит іде з браузера вашого відвідувача, а не з вашого сервера.

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

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

Безпека береться не з приховування адреси. Вона береться з правил, які ви задаєте на іншому кінці: у випадку Supabase це Row Level Security, яка вирішує рядок за рядком, кому що видно. Саме ці правила варто перевіряти, а увімкнути їх не означає бути захищеним.

Саме Row Level Security робить ключ anon безпечним. Ключ service_role проходить повз нього.

Дві родини ключів

ПостачальникКлючБезпечний у браузері?Що він робить
Supabasesb_publishable_…, або anon у старіших проєктахТут доречноКаже, про який проєкт ідеться. Кожен запит і далі фільтрується вашими правилами Row Level Security.
Supabasesb_secret_…, або service_role у старіших проєктахНіколиПовністю обходить Row Level Security. Читає і змінює кожен рядок у кожній таблиці, хоч би що казали ваші правила.
Stripepk_live_… (публічний)Тут доречноСтворює форми оплати. Не може ані переказати гроші, ані прочитати клієнтів.
Stripesk_live_… (секретний)НіколиПовний доступ до акаунта: списання, повернення, виплати, картки клієнтів.
OpenAI / Anthropicбудь-який ключНіколиПублічного варіанту не існує. Кожен ключ виставляє рахунок просто вам.

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

Як розрізнити їх за 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, і вони продовжують працювати; у панелі ви можете навіть бачити обидві пари поруч.

Я вже вставив секретний ключ у застосунок. Що тепер?

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

Автор

Vlad Tkachenko

Засновник Reeve

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

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

Читати далі

Усі статті

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

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

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

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