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

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

Які 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будь-який ключНіколиПублічного варіанту не існує. Кожен ключ виставляє рахунок просто вам.

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

Як розрізнити їх за 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 секунд. Без облікового запису й без картки.

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

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