Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Middle: питання на співбесіді з теми «Захист і інтеграції»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

OAuth 2.0 дозволяє застосунку отримати доступ до даних користувача в іншому сервісі (Google, GitHub) без його пароля: користувач підтверджує доступ на сторінці сервісу, а застосунок отримує токен з обмеженими правами.

Authorization Code flow:

  1. Застосунок перенаправляє користувача на сервер авторизації з client_id, redirect_uri, scope і випадковим state.
  2. Користувач входить і погоджується надати доступ.
  3. Сервер повертає користувача на redirect_uri з одноразовим кодом і тим самим state.
  4. Застосунок перевіряє state (захист від CSRF) і обмінює код на токени прямим запитом сервер-сервер.
  5. Отримує access_token (короткоживучий) і часто refresh_token.

PKCE (Proof Key for Code Exchange) захищає від перехоплення коду. Перед кроком 1 застосунок генерує випадковий code_verifier і передає його хеш - code_challenge. На кроці 4 надсилає сам code_verifier. Сервер перевіряє, що хеш збігається, - тож перехоплений код без verifier'а марний.

code_verifier  = випадковий рядок 43-128 символів
code_challenge = BASE64URL(SHA256(code_verifier)), method = S256

Чому PKCE тепер обов'язковий скрізь: спершу його придумали для мобільних і SPA-застосунків, які не можуть зберігати client_secret. Сучасні рекомендації (OAuth 2.0 Security BCP, OAuth 2.1) вимагають PKCE і для серверних клієнтів.

Чого не використовувати: Implicit flow (токен одразу в URL) і Resource Owner Password flow (застосунок бере пароль користувача) - обидва вважаються застарілими й небезпечними.

У Laravel: вхід через сторонні сервіси - Socialite (він уже передає state і підтримує PKCE), власний OAuth-сервер - Passport.

Докладніше в документації: PKCE

Rate limiting обмежує, скільки запитів клієнт може зробити за проміжок часу. Захищає від перевантаження, перебору паролів і токенів, масового викачування даних і від одного «галасливого» клієнта, що забирає ресурси в інших.

Чим рахувати:

  • за користувачем чи токеном - для автентифікованих запитів, найточніше;
  • за IP - для анонімних (вхід, реєстрація, скидання пароля), з поправкою на NAT і проксі;
  • за ендпоінтом: вхід - 5 спроб на хвилину, пошук - 60, звичайні запити - більше.

Алгоритми: фіксоване вікно (просто, але дозволяє «сплеск» на межі вікон), ковзне вікно, token bucket (дозволяє короткі сплески до місткості «відра» за стабільної середньої швидкості).

Відповідь при перевищенні - 429 Too Many Requests з підказками клієнту:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0

Retry-After каже, через скільки секунд повторити. X-RateLimit-* - поширена (хоч і нестандартна) практика; IETF працює над стандартними заголовками RateLimit і RateLimit-Policy.

У Laravel:

RateLimiter::for('api', fn (Request $request) =>
    Limit::perMinute(60)->by($request->user()?->id ?: $request->ip())
);

Лічильники мають жити в спільному сховищі (Redis), інакше на кількох серверах кожен рахуватиме своє.

Клієнтам - обробляти 429 з експоненційною затримкою й випадковим розкидом (jitter), а не повторювати одразу. Шари захисту: ліміти на рівні CDN/WAF відсікають грубі атаки ще до застосунку, а застосунок обмежує за бізнес-логікою.

Докладніше в документації: RFC 6585: код 429

BOLA (Broken Object Level Authorization, раніше IDOR - Insecure Direct Object Reference) - API перевіряє, що користувач автентифікований, але не перевіряє, чи має він доступ саме до цього об'єкта.

// вразливо: будь-який автентифікований користувач отримає будь-яке замовлення
Route::get('/api/orders/{order}', fn (Order $order) => new OrderResource($order))
    ->middleware('auth:sanctum');

Зловмисник змінює /api/orders/1041 на /api/orders/1040 - і бачить чуже замовлення. Ідентифікатори в API на виду, перебрати їх - справа скрипта.

Чому це вразливість №1 у OWASP API Top 10:

  • у кожному ендпойнті з ідентифікатором треба окремо не забути перевірку;
  • автоматичні сканери її погано знаходять - технічно запит коректний;
  • тести зазвичай перевіряють «власник бачить своє», а не «чужий не бачить».

Захист у Laravel:

1. Політики:

public function show(Order $order): OrderResource
{
    $this->authorize('view', $order);   // OrderPolicy::view
    return new OrderResource($order);
}

// або на маршруті
->can('view', 'order');

2. Пошук через власника - чужий запис просто не знайдеться:

$order = $request->user()->orders()->findOrFail($id);

3. Вкладені маршрути з обмеженням: scopeBindings() - дочірній запис має належати батьківському.

4. Списки - теж об'єкти: GET /api/orders?user_id=5 не повинен повертати замовлення іншого користувача. Фільтр за власником - на сервері, а не з параметра.

Чого не робити:

  • покладатися на непередбачувані ID (UUID замість автоінкременту). UUID ускладнює перебір, але не замінює перевірку: ідентифікатор може потрапити в URL, лог, лист;
  • перевіряти лише в інтерфейсі (прихована кнопка) - API викликають напряму;
  • довіряти ідентифікатору з тіла запиту ("owner_id": 5) - власника визначають з автентифікації.

Тест на кожен ендпойнт з ідентифікатором:

it('forbids viewing another user\'s order', function () {
    $order = Order::factory()->create();
    Sanctum::actingAs(User::factory()->create());

    $this->getJson("/api/orders/{$order->id}")->assertForbidden();
});

Відповідь 403 чи 404: 404 не підтверджує, що об'єкт існує, - для чутливих даних це краще.

Докладніше в документації: OWASP API1:2023 - BOLA

JWT (JSON Web Token) - токен з трьох частин у Base64URL, розділених крапками: заголовок.дані.підпис.

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsImV4cCI6MTc2MDAwMDAwMH0.Sfl...
  • заголовок - алгоритм підпису (alg);
  • дані (claims) - sub (користувач), exp (термін дії), iss, aud, ролі тощо;
  • підпис - гарантує, що дані не змінено.

Ключова властивість: сервер може перевірити токен без звернення до бази - достатньо ключа. Звідси популярність у мікросервісах.

Що варто розуміти:

  • JWT не шифрований (якщо це не JWE) - будь-хто прочитає дані, декодувавши Base64. Не кладіть туди персональних даних і секретів;
  • підпис ≠ секретність: підпис захищає від зміни, а не від читання.

Типові вразливості (RFC 8725 описує найкращі практики):

  • alg: none - бібліотека, що приймає непідписані токени. Сервер має жорстко задавати дозволені алгоритми, а не брати їх із заголовка токена;
  • плутанина алгоритмів: сервер очікує RS256 (асиметричний), а зловмисник підписує HS256, використавши публічний ключ як секрет. Захист - той самий: явний список алгоритмів;
  • слабкий секрет HS256 - коротку фразу перебирають офлайн;
  • відсутня перевірка exp, aud, iss - токен з іншого сервісу чи прострочений приймається;
  • неможливість відкликання: stateless-токен діє до exp, навіть якщо користувач вийшов чи його заблоковано. Ліки - короткий термін (5-15 хвилин) + refresh-токен, або чорний список (jti) - що повертає звернення до сховища;
  • зберігання в localStorage - крадіжка через XSS.

Коли JWT не потрібен: для власного застосунку з одним бекендом - звичайна сесія чи непрозорий токен у базі (як Sanctum) простіші: відкликання миттєве, у токені немає даних, бібліотеки не потрібні.

Коли доречний: кілька сервісів перевіряють один токен без спільної бази; OAuth/OpenID Connect (ID-токени - це JWT); короткоживучі підписані посилання.

У Laravel: Sanctum використовує непрозорі токени з хешем у базі; Passport (OAuth2-сервер) видає JWT-токени доступу.

Докладніше в документації: RFC 8725: найкращі практики JWT

GET /api/export?api_token=sk_live_9f8a...

Токен у рядку запиту «протікає» в місця, які ніхто не захищає як сховище секретів:

  • журнали доступу вебсервера, балансувальника, CDN, проксі - URL записується повністю;
  • історія браузера й закладки;
  • заголовок Referer - при переході з такої сторінки на інший сайт URL може піти туди;
  • системи моніторингу й аналітики (Sentry, APM), що записують URL запитів;
  • кеш проксі - відповідь може закешуватися за URL з токеном;
  • знімки екрана й повідомлення в чатах підтримки.

RFC 6750 прямо не рекомендує передавати bearer-токени в параметрах URL, крім випадків, коли інших варіантів немає.

Правильно - заголовок:

Authorization: Bearer sk_live_9f8a...

Якщо URL без токена неможливий (завантаження файлу за посиланням, WebSocket у браузері, вбудовування зображення) - короткоживучий одноразовий токен чи підписаний URL з терміном дії, прив'язаний до конкретного ресурсу:

URL::temporarySignedRoute('exports.download', now()->plus(minutes: 10), ['export' => $export]);

Що ще не повинно потрапляти в логи API:

  • заголовки Authorization, Cookie, X-Api-Key;
  • паролі, коди підтвердження, одноразові коди 2FA (типово - поля password, token, code у тілі);
  • номери карток, персональні документи, медичні дані;
  • повні тіла відповідей із персональними даними.

Як це забезпечити:

  • маскування в логах централізовано (процесор логів, фільтр полів у Sentry/Telescope), а не «не забути» в кожному місці;
  • у Laravel - $hidden для серіалізації моделей, #[\SensitiveParameter] для параметрів функцій (значення не з'явиться в стеку винятку), налаштування dontFlash для полів, що не повертаються у форму після помилки;
  • токени з префіксом (sk_live_...) - їх легше знайти й замаскувати сканерами секретів. Sanctum підтримує префікс через token_prefix.

Якщо секрет усе ж потрапив у лог чи репозиторій - вважати його скомпрометованим і відкликати, а не лише видалити запис.

Докладніше в документації: RFC 6750: bearer-токени