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

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

Чи є відкритий API-ендпоінт проблемою безпеки? Читайте JSON

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

Vlad Tkachenko11 хв читання
Два однакові віконця у стіні, з кожного визирає аркуш паперу. На одному аркуші ряд перемикачів, на другому стовпчик облич.

Коротко

  • Відкритий API-ендпоінт означає, що шлях, записаний у коді вашого застосунку, відповів на запит без логіна, і повернувся JSON.
  • Із тих, що ми знайшли у 30 926 застосунках, семеро з десяти поклала туди платформа, на якій застосунок опубліковано, і вони віддають налаштування.
  • Вирішує одне питання: чи описує щось у відповіді людину. Прайс-лист є публічним навмисно. Список ваших клієнтів таким не був ніколи.

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

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

Що відкритий API-ендпоінт означає у вашому звіті

Це означає, що шлях, записаний у коді вашого застосунку, відповів на запит, який не ніс логіна, і що повернувся JSON.

Три кроки, і жоден із них не хитрий. Ми завантажуємо ваш застосунок у браузері так само, як це робить відвідувач, і читаємо код, який той браузер завантажив. Усередині лежать адреси, до яких ваш застосунок звертається, коли йому потрібні дані. Тоді ми просимо дані в кожної з них, без облікового запису, без cookie і без ключа. Якщо адреса відповідає JSON, який не є повідомленням про помилку, вона потрапляє у звіт.

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

Адреси беруться з вашого ж коду, і в кожної питають, нічого не додаючи. Те, що відповість, і потрапляє у звіт.

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

Чи кожен відкритий API-ендпоінт є проблемою безпеки?

Ні. Їх три різновиди, і проблемою є лише третій.

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

Що відповілоЧия це адресаДе ви стоїте
Налаштування платформи: cookie-банери, перемикачі функційВашого білдера, у кожному застосунку, який він видаєНічого робити
Ваші власні публічні дані: прайс-лист, статтяВаша, і призначена для читанняНічого робити
Рядки про людей: імена, пошта, замовлення, повідомленняВаша, і доступна кожному, хто має адресуЗакрити сьогодні

Перший рядок це дошка оголошень, яку прикрутив до стіни хтось інший. Другий це дошка, яку ви повісили навмисно. Третій це шафа з документами, і вона стоїть відчиненою стільки, скільки існує адреса.

Як перевірити, що віддають ваші ендпоінти

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

Знайдіть адреси. Відкрийте ваш застосунок у продакшені, натисніть F12, щоб викликати інструменти розробника браузера, перейдіть на вкладку Мережа і перезавантажте сторінку. Перегляньте запити, які повертаються як JSON. Це адреси, з яких ваш застосунок бере дані, а перелік у звіті є їхньою підмножиною.

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

Прочитайте перші рядки. Повернутися може одне з трьох:

  • Помилка, відмова або []. Адреса хоче логін, або для розлогіненого відвідувача там нічого немає. Ідіть далі.
  • Налаштування. Прапорці, перемикачі, колір, перелік увімкнених функцій, номер версії. Нікого не названо і нічого не описано. Ідіть далі.
  • Рядки. Поштова адреса, ім'я людини, сума замовлення, текст повідомлення, номер телефону. Зупиніться, ось вона.

Якщо ви знайшли рядки, спробуйте змінити число в URL, перш ніж закрити вкладку. Адреса, яка на id=1 видає запис однієї людини, а на id=2 іншої, видає їх усі, і це варто знати, перш ніж вирішувати, наскільки це терміново.

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

Більшість знайденого належить платформі

У нашому серпневому обході 2026 року 3 852 із 30 926 застосунків, де ця перевірка змогла отримати відповідь, мали щонайменше одну адресу, яка відповідає без логіна. Із них 2 705 були застосунками Base44, і в Base44 адреса майже завжди та сама.

Це /api/consent/config, налаштування cookie цієї платформи. У вересні ми повторно відкрили вибірку позначених застосунків Base44, і єдиною адресою, яка відповіла, була саме ця. Вона віддає налаштування, вона однакова в усіх застосунках, і нічиїх даних у ній немає. Сканер повідомляє про неї правильно, а власникові нема чого виправляти.

ПлатформаЗастосунків із висновкомЗастосунків, де перевірка відповіла
Base442 7055 419
Власні домени82955
Bolt41 120
Lovable818 518
v001 786

Однієї платформи в цій таблиці немає навмисно. На Replit припадає великий блок решти висновків, і ніхто не відкривав їхньої вибірки повторно так, як ми зробили це з Base44, тож ми ще не знаємо, чиї це адреси. Публікувати число як проблему власника, перш ніж хтось на нього подивився, це рівно той шлях, яким налаштування платформи Base44 перетворюється на статистику про недбалих власників. Кожне число вище походить із нашого сканування 30 998 живих vibe-coded застосунків, яке публікує кожне разом із базою, на якій його виміряли.

Прочитайте обидва кінці цієї таблиці разом, бо корисною є саме різниця. У Lovable це 8 застосунків із 18 518, а в v0 жодного. У таких застосунків немає свого маленького сервера: сторінка говорить із Supabase просто з браузера, тож ніде немає вашого /api/…, який ця перевірка могла б знайти. Дані в такому застосунку захищають правила на кожній таблиці, а це вже інший висновок зі своїм способом ламатися.

Коли JSON це налаштування, а коли це люди

Вирішує одне питання: чи описує щось у відповіді людину?

Налаштування описують ваш застосунок. Прапорець про те, чи увімкнено темну тему, перелік валют, які ви приймаєте, версія чогось, текст для cookie-банера. Публікація цього видає, як налаштований ваш застосунок, а ваш застосунок і так працює у всіх на очах.

Рядки описують ваших користувачів. Ім'я, поштова адреса, що людина купила, що написала, де живе. Це не речення про конфігурацію, це вміст тієї шафи з документами, і важить воно через те, наскільки буденно його забрати. Жодного зламу немає. Адреса це рядок тексту, який працює в будь-якому браузері, тож її вставляють у груповий чат, зберігають у чиїхось нотатках і за розкладом завантажують скриптом, за яким ніхто не стежить.

Обидві є кодом 200 і блоком JSON, і це все, що бачить перевірка. Різниця між ними і є цілим питанням, а відповісти на нього можна читанням.

Як закрити ендпоінт і чому перейменування шляху його не закриває

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

У цьому все виправлення, і місце йому на самій адресі, а не деінде. Три речі, які виглядають як виправлення і ним не є:

  • Перейменувати шлях або прибрати його з коду. Саме цього варто стерегтися, бо висновок зникає. У кого URL уже був, у того він і лишився, він є в історіях браузерів і серверних логах, а шафа й далі стоїть незамкненою, тільки без таблички спереду.
  • Звузити ваш заголовок CORS. Він вирішує, які інші сайти можуть прочитати відповідь усередині чийогось браузера, і більше його ніщо не читає. Скрипт або термінал читає ту саму адресу в обох випадках, і саме тому висновок про вайлдкард є іншим питанням.
  • Фільтрувати в застосунку. Якщо сторінка ховає рядки, яких не має показувати, адреса однаково їх надсилає. Фільтрувати треба до того, як відповідь піде назовні.

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

Який вигляд виправлення має в коді

Дві речі в обробнику, який відповідає на адресу: з'ясувати, хто питає, і зупинитися, якщо відповідь це ніхто.

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

app.get('/api/orders', async (req, res) => {
  const orders = await db.orders.findMany()
  res.json(orders)
})

А ось після:

app.get('/api/orders', async (req, res) => {
  const user = await getUserFromRequest(req)
  if (!user) {
    return res.status(401).json({ error: 'Not signed in' })
  }

  const orders = await db.orders.findMany({ where: { userId: user.id } })
  res.json(orders)
})

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

В Edge Functions від Supabase спершу прочитайте конфігурацію, а вже потім щось пишіть. Edge Functions перевіряють токен того, хто звертається, самостійно: довідник конфігурації Supabase ставить verify_jwt у true за замовчуванням, тож функція, яка відповідає незнайомцям, це функція, де хтось це вимкнув, або рядком у supabase/config.toml, або деплоєм із --no-verify-jwt:

[functions.orders]
verify_jwt = false

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

import { withSupabase } from 'npm:@supabase/server'

export default {
  fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
    const { data } = await ctx.supabase.from('orders').select()
    return Response.json(data)
  }),
}

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

Промпт, який треба вставити у ваш білдер

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

A security scan of [my-app.com] found these API endpoints answer
without any authentication: [/api/orders, /api/users].

Please look at each one and decide whether it is meant to be public.
For the ones that are not, require a valid session or API token and
return 401 otherwise. Make sure each one returns only the rows that
belong to the person asking, not the whole table filtered in the UI.
For any that must stay open, make sure they return only non-sensitive
data and add rate limiting so they cannot be abused.

Tell me what you changed for each endpoint, and do not rename or
remove the paths.

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

Що робити просто зараз

Що робити

  • Відкрийте кожну позначену адресу в приватному вікні браузера і прочитайте, що повертається. Це той крок, якого звіт за вас не зробить.
  • Якщо у відповіді налаштування, а адресу ви ніколи не писали, вона належить вашому білдеру і виправляти нічого не треба. Пошукайте цей шлях у документації вашої платформи, перш ніж щось на нього витрачати.
  • Якщо у відповіді є ім'я, поштова адреса чи замовлення, вимагайте логін на цій адресі вже сьогодні й повертайте 401 кожному, хто його не має.
  • Не перейменовуйте шлях і не видаляйте посилання на нього. Висновок зникне, а адреса відповідатиме далі.
  • Спробуйте змінити id в URL у вашому власному застосунку. Адреса, яка віддає незнайомцю один запис, зазвичай віддає їх усі.

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

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

Поширені запитання

Що означає "відкритий API-ендпоінт" у моєму звіті?

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

Чи кожен відкритий API-ендпоінт є проблемою безпеки?

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

Мій звіт позначає ендпоінт, якого я не писав. Що це?

Зазвичай це ендпоінт вашого білдера. Платформи, які дають вашому застосунку маленький власний сервер, кладуть у нього і кілька своїх адрес, а ті лежать у коді кожного застосунку цієї платформи. Найчіткіший приклад, який ми виміряли, це налаштування cookie від Base44 за адресою /api/consent/config, на які припадає 2 705 із 3 852 висновків нашого серпневого обходу 2026 року. Вони віддають налаштування, вони однакові в кожному застосунку Base44, і там немає нічого вашого.

Як перевірити, що віддають ендпоінти мого застосунку?

Відкрийте ваш застосунок у продакшені, натисніть F12, перейдіть на вкладку Мережа і перезавантажте сторінку. Запити, які повертаються як JSON, це адреси, з яких ваш застосунок бере дані. Скопіюйте кожен URL, відкрийте приватне вікно браузера, щоб бути розлогіненим, і вставляйте їх по одному. Читайте те, що приходить. Помилка або порожній список це нормально. Рядки з іменами, поштовими адресами чи сумами замовлень може прочитати будь-хто, у кого є та адреса.

Чи допоможе сховати шлях?

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

Автор

Vlad Tkachenko

Засновник Reeve

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

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

Читати далі

Усі статті

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

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

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

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