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

Питання на співбесіді: Автентифікація й секрети

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

15 питань

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

Чому не шифрування: зашифроване можна розшифрувати. Хто отримав ключ разом з базою, отримав усі паролі. Хеш - односторонній: навіть сервер не знає пароля, лише вміє перевірити.

Чому не md5/sha256: вони створені швидкими. Сучасна відеокарта перебирає мільярди хешів SHA-256 за секунду, тож витеклу базу з простими паролями підбирають за години.

Правильні функції - навмисно повільні й налаштовувані:

  • Argon2id - рекомендований вибір;
  • bcrypt - перевірений, широко підтримуваний (обмеження: використовує лише перші 72 байти пароля).
$hash = password_hash($password, PASSWORD_ARGON2ID);   // або PASSWORD_DEFAULT (bcrypt)

if (password_verify($input, $hash)) {
    // вхід
}

Hash::make($password);      // Laravel: bcrypt за замовчуванням, налаштовується
Hash::check($input, $hash);

Сіль - випадкове значення, унікальне для кожного пароля. Вона робить однакові паролі різними хешами й унеможливлює готові таблиці (rainbow tables). password_hash генерує сіль сам і зберігає її в рядку хешу - окрема колонка не потрібна.

Ще правила:

  • Перехешування при вході (password_needs_rehash, у Laravel - автоматично), коли збільшується вартість чи змінюється алгоритм.
  • Ліміт спроб входу, щоб онлайн-перебір був непрактичним.
  • Перевірка на злиті паролі (Have I Been Pwned, правило Password::uncompromised() у Laravel) замість вимог «велика літера + цифра + символ».

Докладніше в документації: OWASP: зберігання паролів

Cookie сесії - фактично ключ до облікового запису. Атрибути визначають, хто й коли може його отримати:

  • HttpOnly - cookie недоступна з JavaScript (document.cookie). XSS не зможе її вкрасти. Для cookie сесії - обов'язково.
  • Secure - cookie передається лише по HTTPS. Без нього її можна перехопити в незашифрованому з'єднанні.
  • SameSite - чи надсилати cookie в запитах з інших сайтів:
    • Lax (Chromium застосовує його до cookie без атрибута, але покладатися на це не варто - ставте явно) - так при звичайних переходах за посиланням, ні - у міжсайтових POST, fetch, iframe. Добрий захист від CSRF.
    • Strict - ніколи в міжсайтових запитах. Безпечніше, але користувач, що перейшов за посиланням з пошти, опиниться незалогіненим.
    • None - завжди (лише разом із Secure). Потрібно для вбудованих віджетів і SSO між різними доменами.
  • Domain - без нього cookie належить лише точному хосту. Domain=example.com відкриває її всім піддоменам, зокрема менш захищеним.
  • Path, Max-Age / Expires - область і термін дії.
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax

Префікси назв - додатковий захист, який перевіряє сам браузер:

  • __Host-session - лише з Secure, без Domain, з Path=/: cookie не може встановити чи перезаписати піддомен.
  • __Secure- - лише з Secure.

У Laravel це налаштовується в config/session.php: secure (SESSION_SECURE_COOKIE=true на проді), http_only, same_site, domain. Cookie застосунку ще й шифруються, тож їхній вміст не прочитати й не підробити без APP_KEY.

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

Багатофакторна автентифікація (MFA) - вхід вимагає доказів з різних категорій:

  • те, що ви знаєте - пароль, PIN;
  • те, що ви маєте - телефон із застосунком, апаратний ключ;
  • те, чим ви є - відбиток пальця, обличчя (зазвичай як розблокування пристрою).

Два паролі - не два фактори: обидва з однієї категорії й крадуться однаково.

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

Види другого фактора - від слабшого до сильнішого:

  • SMS-коди - краще, ніж нічого, але вразливі: перевипуск SIM-картки (SIM swap) через оператора, перехоплення, фішинг коду;
  • email-коди - залежать від безпеки пошти;
  • TOTP (Google Authenticator, 1Password, Authy) - код з 6 цифр, що змінюється кожні 30 секунд. Обчислюється на пристрої з спільного секрету й поточного часу (RFC 6238) - не передається мережею, не залежить від оператора;
  • push-підтвердження - зручно, але вразливе до «втоми від сповіщень» (зловмисник надсилає десятки запитів, поки користувач не натисне «Так»). Захист - введення числа з екрана входу;
  • апаратні ключі й passkeys (WebAuthn) - стійкі до фішингу, бо прив'язані до домену сайту.

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

Обов'язкові частини реалізації:

  • резервні коди на випадок втрати телефону - одноразові, зберігаються хешованими;
  • захищене відновлення доступу - найслабше місце MFA: якщо «втратив телефон» вирішується одним листом, атакують саме його;
  • обмеження спроб введення коду (6 цифр - лише мільйон варіантів);
  • код діє один раз у своєму вікні часу;
  • повторне підтвердження перед вимкненням MFA чи зміною пошти.

У Laravel двофакторна автентифікація з TOTP і резервними кодами є в Laravel Fortify і стартових наборах.

Докладніше в документації: OWASP: багатофакторна автентифікація

Дві різні атаки на вхід:

  • перебір (brute force) - багато паролів для одного облікового запису;
  • credential stuffing - пари «email + пароль» з витоків інших сайтів перевіряються на вашому. Працює, бо люди повторюють паролі. Спроб на один обліковий запис мало - тож простий ліміт на обліковий запис цю атаку не бачить.

Захист - кілька шарів:

1. Обмеження частоти - за комбінацією облікового запису й IP і окремо за IP:

RateLimiter::for('login', fn (Request $request) => [
    Limit::perMinute(5)->by(Str::lower($request->input('email')).'|'.$request->ip()),
    Limit::perMinute(30)->by($request->ip()),
]);

Laravel Fortify і стартові набори вже обмежують спроби входу.

2. Багатофакторна автентифікація - найефективніший захист від обох атак: правильний пароль без другого фактора нічого не дає.

3. Заборона скомпрометованих паролів при реєстрації й зміні пароля:

'password' => ['required', 'confirmed', Password::min(12)->uncompromised()],

uncompromised() перевіряє пароль у базі витоків Have I Been Pwned методом k-анонімності: на сервіс надсилаються лише перші 5 символів SHA-1-хешу, а не сам пароль.

4. Виявлення аномалій: вхід з нової країни чи пристрою - сповіщення користувачу, додаткова перевірка.

5. CAPTCHA чи невидимі перевірки (Cloudflare Turnstile) - після кількох невдач чи при підозрілому трафіку, а не завжди.

6. Захист на рівні мережі: WAF, ліміти на CDN, блокування відомих ботнетів.

Чого уникати:

  • постійне блокування облікового запису після N невдач - зловмисник легко заблокує будь-кого (DoS для користувачів). Краще тимчасові затримки й CAPTCHA;
  • різні повідомлення «невірний пароль» і «користувача не знайдено» - це перелік зареєстрованих email;
  • ліміт лише за IP - атакують через тисячі адрес;
  • ліміт лише за обліковим записом - credential stuffing його не досягає.

Моніторинг: різке зростання невдалих входів з різних IP на різні облікові записи - ознака credential stuffing, потрібна реакція (посилені перевірки, повідомлення користувачам).

Докладніше в документації: OWASP: запобігання credential stuffing

Перелік облікових записів - можливість дізнатися, чи зареєстрована певна адреса email (чи логін) у системі. Сама по собі це не злам, але:

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

Де це протікає:

1. Повідомлення входу:

«Користувача з таким email не знайдено»    ← протікає
«Невірний пароль»                          ← протікає
«Невірний email або пароль»                ← правильно

2. Скидання пароля:

«Лист надіслано» / «Такого email немає»                       ← протікає
«Якщо обліковий запис існує, ми надіслали інструкції на email»  ← правильно

3. Реєстрація: «Email уже зайнятий» - складніше уникнути, бо користувачу треба знати, що він уже зареєстрований. Варіант - завжди відповідати «Перевірте пошту», а існуючому користувачу надіслати лист «Ви вже маєте обліковий запис, ось посилання на вхід».

4. Час відповіді. Навіть з однаковими повідомленнями: якщо для неіснуючого користувача сервер відповідає за 5 мс, а для існуючого - 300 мс (бо перевіряє хеш bcrypt), час видає правду. Захист - виконувати перевірку хешу завжди (з фіктивним хешем, якщо користувача немає) і відправляти листи асинхронно з черги, щоб час відповіді на скидання пароля не залежав від того, чи надсилається лист.

5. API й публічні профілі: GET /api/users/check-email?email=... для «живої» перевірки в формі, профілі за передбачуваними URL.

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

У Laravel: стандартний ValidationException з auth.failed дає загальне повідомлення, а Password::sendResetLink повертає статус, який варто показувати однаково для всіх випадків. Fortify за замовчуванням дотримується цих правил, але власні контролери варто перевірити.

Докладніше в документації: OWASP: шпаргалка з автентифікації

Сесія: сервер зберігає стан (в базі, Redis, файлах), а клієнт має лише випадковий ID у cookie. Кожен запит - пошук сесії за ID.

  • Миттєве відкликання: видалили сесію - доступу немає.
  • Потрібне спільне сховище для кількох серверів.

JWT: токен сам містить дані (ID користувача, ролі, термін дії) і підписаний сервером. Перевірка - лише підпис, без звернення до сховища.

  • Зручно між сервісами й доменами: будь-який сервіс з ключем перевірки довіряє токену.
  • Відкликати до закінчення терміну складно: токен дійсний, поки не сплив. Звідси короткий термін access-токена (хвилини) плюс refresh-токен, або список відкликаних - що повертає стан на сервер.
  • Дані в JWT не зашифровані, лише підписані: base64 розкодує будь-хто.

Де зберігати токен у браузері:

  • localStorage - доступний будь-якому JavaScript на сторінці. Одна XSS-вразливість - і токен викрадено й використано з іншого місця.
  • Cookie з HttpOnly, Secure, SameSite - JavaScript токен не бачить. XSS усе ще може робити запити від імені користувача, але не може винести токен. Потрібен захист від CSRF (SameSite + токен).

Для власного SPA на тому самому домені найпростіше й надійніше - звичайна сесія в cookie (як Sanctum у SPA-режимі). JWT і bearer-токени доречні для мобільних застосунків, інтеграцій сервер-сервер, мікросервісів.

Пастки JWT (RFC 8725): приймати лише очікуваний алгоритм (атака з alg: none і підміною алгоритму), перевіряти exp, iss, aud, не класти в токен секретних даних.

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

Фіксація сесії (session fixation): нападник заздалегідь отримує ID сесії (просто відкривши сайт) і змушує жертву використовувати цей самий ID - через посилання з параметром, вразливість на піддомені, що встановлює cookie. Жертва входить в обліковий запис, сесія стає автентифікованою - а її ID нападник уже знає.

Захист - новий ID сесії при зміні рівня привілеїв: після входу, після підвищення прав (вхід в адмінку, підтвердження пароля), після виходу.

if (Auth::attempt($credentials)) {
    $request->session()->regenerate();   // новий ID, дані сесії збережено
    return redirect()->intended('/dashboard');
}

// вихід
Auth::logout();
$request->session()->invalidate();       // знищити сесію
$request->session()->regenerateToken();  // новий CSRF-токен

Стартові набори Laravel (Breeze, Jetstream, Fortify) роблять це автоматично; у власній логіці входу про це легко забути.

Інші правила керування сесіями:

  • ID лише в cookie (HttpOnly, Secure, SameSite), ніколи в URL: звідти він потрапляє в логи, історію браузера й заголовок Referer.
  • Не приймати ID сесії, які сервер не видавав (строгий режим, session.use_strict_mode у PHP).
  • Тайм-аути: неактивності (наприклад, 30 хвилин для чутливих застосунків) і абсолютний.
  • Вихід на всіх пристроях після зміни пароля (Auth::logoutOtherDevices() у Laravel).
  • Повторне підтвердження пароля перед критичними діями - зміною email, видаленням облікового запису.

Докладніше в документації: OWASP: керування сесіями

Скидання пароля - «чорний хід» в обліковий запис. Якщо він слабший за вхід, атакують саме його.

Вимоги до токена скидання:

  • випадковий і довгий - криптографічно стійкий генератор (random_bytes, Str::random()), не похідний від email, часу чи id;
  • одноразовий - після використання недійсний;
  • короткий термін дії - хвилини чи година;
  • у базі - хеш токена, а не сам токен: витік бази не дає активних посилань для скидання;
  • прив'язаний до облікового запису - токен одного користувача не працює для іншого.

Процес:

  1. користувач вводить email - відповідь однакова незалежно від того, чи існує обліковий запис («Якщо обліковий запис існує, ми надіслали лист»);
  2. лист із посиланням, що містить токен; відправка - через чергу (щоб час відповіді не видавав існування облікового запису);
  3. перехід за посиланням - форма нового пароля; токен перевіряється порівнянням хешів у постійному часі;
  4. новий пароль - з тими самими правилами, що й при реєстрації (довжина, перевірка у базі витоків);
  5. після зміни: токен знищено, усі інші сесії й токени API користувача відкликано, лист-сповіщення «ваш пароль змінено».

Пастки:

  • отруєння посилання через заголовок Host: якщо URL у листі будується з Host запиту, а сервер приймає довільні хости, зловмисник ініціює скидання для жертви з підробленим Host: evil.example - жертва отримує справжній лист з посиланням на домен зловмисника. Захист - фіксований APP_URL для посилань і перевірка дозволених хостів;
  • витік токена через Referer: сторінка скидання завантажує сторонні ресурси (аналітику, шрифти) - токен з URL потрапляє в Referer. Referrer-Policy: no-referrer на цій сторінці;
  • відповідь на секретні питання замість пошти - слабкий механізм, відповіді легко знайти в соцмережах;
  • автоматичний вхід після скидання без MFA - обхід другого фактора;
  • необмежена кількість запитів - спам листами на чужу адресу.

У Laravel брокер паролів (Password::sendResetLink, Password::reset) зберігає хеш токена в password_reset_tokens, має термін дії й обмеження частоти (throttle у config/auth.php). Після скидання варто викликати Auth::logoutOtherDevices() чи прибрати сесії й токени Sanctum.

Докладніше в документації: OWASP: забутий пароль

Passkey - облікові дані на основі криптографії з відкритим ключем (стандарт WebAuthn/FIDO2) замість пароля.

Як це працює:

  1. реєстрація: пристрій користувача (телефон, ноутбук, апаратний ключ) створює пару ключів для конкретного сайту. Приватний ключ лишається на пристрої (в захищеному сховищі), сайт отримує й зберігає лише публічний;
  2. вхід: сервер надсилає випадковий виклик (challenge), пристрій підписує його приватним ключем після локального підтвердження (відбиток, обличчя, PIN пристрою), сервер перевіряє підпис публічним ключем.

Чому це сильніше за пароль + код:

  • стійкість до фішингу. Ключ прив'язаний до домену сайту (relying party ID). Браузер просто не дасть використати ключ для laravelukraine.com на сторінці laravelukraine-login.com. Фальшивий сайт нічого не отримає - на відміну від пароля й навіть TOTP-коду, які користувач введе куди завгодно;
  • нічого вкрасти з сервера: у базі лише публічні ключі - їх витік нікому не допомагає;
  • немає повторного використання: кожен сайт має свою пару ключів;
  • немає перебору й credential stuffing - немає пароля;
  • два фактори в одному: «маю пристрій» + «біометрія/PIN пристрою».

Синхронізовані й прив'язані до пристрою:

  • синхронізовані passkeys (iCloud Keychain, Google Password Manager, 1Password) - доступні на всіх пристроях користувача, не губляться разом з телефоном;
  • прив'язані до пристрою (апаратні ключі YubiKey) - максимальна безпека, але потрібен резервний ключ.

Що врахувати при впровадженні:

  • відновлення доступу: втратили всі пристрої - потрібен запасний шлях (резервний passkey, перевірена пошта з додатковими перевірками), і він не повинен бути слабшим за сам passkey;
  • поступовий перехід: спершу passkey як додатковий спосіб входу чи другий фактор, пароль поки лишається;
  • кілька passkeys на обліковий запис - з назвами пристроїв і можливістю видалити;
  • бібліотеки: реалізовувати WebAuthn вручну не варто - перевірка підпису, лічильників, атестації має багато тонкощів. Для PHP/Laravel є готові пакети, на фронтенді - API браузера navigator.credentials.

Підтримка: усі сучасні браузери й операційні системи; великі сервіси (Google, Apple, GitHub, Microsoft) уже пропонують вхід без пароля.

Докладніше в документації: Passkeys.dev: що таке passkeys

«Запам'ятати мене» - користувач лишається автентифікованим тижні й місяці, навіть після закриття браузера, коли звичайна сесія вже завершилася.

Як це роблять правильно:

  • окремий довгоживучий токен у cookie - випадковий, криптографічно стійкий, а не email, id чи хеш пароля;
  • у базі - хеш токена (чи токен, прив'язаний до користувача, як у Laravel - remember_token), щоб витік бази не давав готових cookie;
  • cookie з HttpOnly, Secure, SameSite - недоступна JavaScript і не передається по HTTP;
  • при використанні токен створює нову звичайну сесію;
  • при виході токен знищується на сервері, а не лише стирається cookie в браузері.

Ризики довгих сесій і що з ними робити:

  • викрадена cookie діє довго. Обмежити абсолютний термін (наприклад, 30 днів), а для чутливих дій вимагати повторного підтвердження паролем (зміна email, пароля, MFA, платіжних даних) - так працює password.confirm middleware в Laravel;
  • вихід «з усіх пристроїв» має справді анулювати remember-токени. У Laravel зміна remember_token (наприклад, при зміні пароля чи Auth::logoutOtherDevices()) робить старі cookie недійсними;
  • «ротація» токена при кожному використанні з виявленням повторного використання - так помічають, що cookie скопійовано;
  • список активних сесій у налаштуваннях облікового запису з пристроєм, місцем і часом останньої активності - користувач бачить чужий вхід і може його завершити (драйвер сесій database дає таку можливість).

Таймаути сесій (рекомендації OWASP):

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

Регенерація ідентифікатора сесії після входу, зміни прав і виходу - захист від фіксації сесії ($request->session()->regenerate()).

Компроміс: «Запам'ятати мене» - зручність коштом безпеки. Для публічних і спільних комп'ютерів його не варто вмикати за замовчуванням; для застосунків з чутливими даними варто обмежити або вимагати MFA при відновленні сесії з remember-токена.

Докладніше в документації: OWASP: керування сесіями

Три різні задачі, які часто плутають:

Хешування - одностороннє перетворення даних у відбиток фіксованої довжини. Відновити дані з хешу неможливо.

  • Перевірка цілісності: чи не змінився файл (SHA-256).
  • Паролі - але лише повільними функціями з сіллю (Argon2id, bcrypt), не швидкими.

Шифрування - двостороннє: з ключем дані можна розшифрувати.

  • Симетричне (AES-GCM, ChaCha20-Poly1305): один ключ шифрує й розшифровує. Швидке; для даних у базі, файлів, cookie.
  • Асиметричне (RSA, X25519): публічний ключ шифрує, лише приватний розшифровує. Для обміну ключами й шифрування для конкретного отримувача.

Використовують автентифіковане шифрування (AEAD): воно не лише приховує дані, а й виявляє їхню зміну. Шифрування без перевірки цілісності (AES-CBC без MAC) вразливе до підміни шифротексту.

Підпис / MAC - доводить, що дані не змінено і створено тим, хто має ключ. Дані при цьому лишаються відкритими.

  • HMAC (симетричний): спільний секрет - підписані URL, підписи вебхуків, JWT з HS256.
  • Цифровий підпис (Ed25519, ECDSA, RSA): приватний ключ підписує, будь-хто з публічним - перевіряє. JWT з RS256/EdDSA, підписи релізів, TLS-сертифікати.

У Laravel:

Crypt::encryptString($iban);       // AES-256-CBC + HMAC (або GCM): шифрування з перевіркою цілісності
Hash::make($password);             // повільний хеш пароля
URL::signedRoute('unsubscribe', ['user' => $id]);   // HMAC-підпис

Головні правила: не винаходити власну криптографію, використовувати перевірені бібліотеки (libsodium, вбудовані засоби фреймворку), зберігати ключі окремо від даних і мати план ротації ключів.

Докладніше в документації: OWASP: криптографічне зберігання

Застосунок - це ваш код плюс сотні пакетів Composer і npm, кожен з яких може мати власні залежності. Атака на ланцюжок постачання - компрометація не вашого коду, а чогось, від чого він залежить: пакета, інструмента збирання, CI.

Як це відбувається:

  • Викрадений обліковий запис мейнтейнера - і в популярний пакет потрапляє шкідлива версія.
  • Typosquatting - пакет з назвою, схожою на популярну.
  • Dependency confusion - публічний пакет з назвою вашого внутрішнього, який менеджер пакетів вибирає замість приватного.
  • Шкідливі скрипти встановлення - postinstall у npm виконується одразу під час npm install.
  • Компрометація CI - секрети з пайплайна, підміна артефактів збирання.

Захист:

  • Lock-файли в репозиторії (composer.lock, package-lock.json) і встановлення лише з них у CI й на проді (composer install, npm ci).
  • Аудит вразливостей: composer audit, npm audit, Dependabot чи Renovate з автоматичними pull request на оновлення.
  • Оновлення - регулярно, але не наосліп: читати changelog, дати новій версії кілька днів «відлежатися», переглядати diff у критичних пакетах.
  • Менше залежностей: кожен пакет - ще одна точка довіри. Для дрібної функції часто простіше написати кілька рядків, ніж додати пакет.
  • Обмеження скриптів: npm ci --ignore-scripts, де можливо; у Composer плагіни потребують явного дозволу (allow-plugins).
  • Захист CI: мінімальні права токенів, секрети лише для гілок, яким довіряєте, закріплення сторонніх GitHub Actions за хешем коміту, а не тегом.
  • Перевірка походження: підписи й attestations пакетів (npm provenance, Sigstore), SBOM для розуміння, що саме в застосунку.

Висновок: залежність - це чужий код, який ви запускаєте з повними правами застосунку. Ставитися до неї варто відповідно.

Докладніше в документації: OWASP: керування вразливими залежностями

Атака за часом - зловмисник вимірює, скільки часу сервер обробляє запит, і з різниці робить висновки про секрет.

Класичний приклад - порівняння рядків:

if ($providedToken === $storedToken) { ... }

Звичайне порівняння зупиняється на першому символі, що не збігся. Токен, у якого правильні перші 10 символів, перевіряється трохи довше, ніж токен з неправильним першим символом. Вимірюючи час тисяч запитів і усереднюючи шум, теоретично можна підбирати токен посимвольно: замість 62^32 варіантів - 62 × 32 спроби.

hash_equals($known, $user) порівнює рядки за постійний час - незалежно від того, де саме відрізняються символи:

if (hash_equals($storedSignature, $providedSignature)) { ... }

Порядок аргументів важливий: перший - відоме (секретне) значення, другий - від користувача.

Де потрібне порівняння в постійному часі:

  • підписи вебхуків і запитів (HMAC);
  • API-ключі й токени, якщо порівнюються як рядки;
  • коди підтвердження, CSRF-токени;
  • підписані URL.

Laravel використовує hash_equals для перевірки CSRF-токенів, підписаних маршрутів, токенів Sanctum.

Інші джерела витоку через час:

  • перевірка пароля лише для існуючих користувачів: для неіснуючого email сервер відповідає миттєво, для існуючого - після bcrypt (сотні мілісекунд). Це дає перелік облікових записів. Захист - виконувати хешування завжди;
  • різна робота залежно від результату: відправка листа лише якщо обліковий запис існує, запит до бази лише для певних випадків;
  • пошук у базі за секретом: WHERE token = ? - час пошуку за індексом теоретично теж може залежати від значення. Тому надійніше шукати за ідентифікатором, а секрет порівнювати окремо через hash_equals (так зроблено в Sanctum: токен має вигляд id|секрет).

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

Не лише рядки: криптографічні реалізації мають бути «constant-time» загалом - тому використовують перевірені бібліотеки (sodium_*, openssl_*), а не власні реалізації алгоритмів.

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

«Увійти через Google/GitHub» здається простим, бо бібліотека (у Laravel - Socialite) бере на себе протокол. Але кілька помилок у логіці застосунку роблять його небезпечним.

1. Об'єднання облікових записів за email без перевірки.

$user = User::firstOrCreate(['email' => $socialUser->getEmail()], [...]);
Auth::login($user);

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

  • зв'язувати лише з підтвердженим у провайдера email (наприклад, email_verified в OpenID Connect);
  • зберігати ідентифікатор провайдера (provider + provider_id), а не лише email, і шукати насамперед за ним;
  • прив'язку соцмережі до існуючого облікового запису робити з облікового запису (користувач уже увійшов паролем) чи з підтвердженням поштою.

2. Відсутність перевірки state. Параметр state пов'язує відповідь провайдера із сесією користувача, що почав вхід. Без нього можлива CSRF-атака на вхід: жертву непомітно входять в обліковий запис зловмисника (і вона, наприклад, зберігає туди свої дані). Socialite перевіряє state за замовчуванням; stateless() вимикає перевірку - лише для API з PKCE.

3. Відкрите перенаправлення. Параметр «куди повернутися після входу» (?redirect=) без перевірки - фішинг на вашому домені. Дозволений лише відносний шлях свого сайту.

4. Неправильні redirect URI в налаштуваннях провайдера. Шаблони (https://*.example.com/*) чи зайві адреси дозволяють перехопити код авторизації. Точні адреси, лише HTTPS.

5. Довіра до даних профілю. Ім'я, аватар, email від провайдера - звичайні дані користувача: їх треба валідувати й екранувати.

6. Втрата доступу при видаленні облікового запису провайдера. Користувач без пароля, що втратив Google-акаунт, втрачає доступ - потрібні альтернативні способи входу.

7. Зайві права (scopes). Просити лише потрібне (openid email profile), а не доступ до пошти чи диска «на майбутнє».

8. Токени провайдера. Якщо застосунку справді потрібні токени доступу до API провайдера - зберігати їх зашифрованими (encrypted cast) і оновлювати refresh-токенами.

Для співбесіди: найважливіше - пункт 1. Захоплення облікових записів через неперевірений email - найпоширеніша реальна вразливість соціального входу.

Докладніше в документації: OWASP: шпаргалка з OAuth 2.0

Для безпеки потрібна не просто «випадковість», а непередбачуваність: знаючи попередні значення, неможливо вгадати наступні.

Некриптографічні генератори - mt_rand(), rand(), uniqid(), lcg_value(), Math.random() у JavaScript:

  • побудовані для швидкості й рівномірного розподілу, а не для безпеки;
  • внутрішній стан відновлюється з кількох виходів - для Mersenne Twister (mt_rand) відомі інструменти, що за кількома значеннями обчислюють зерно й передбачають усі наступні;
  • uniqid() - це просто поточний час у мікросекундах: передбачуваний.

Криптографічно стійкі генератори (CSPRNG) - з джерела ентропії операційної системи:

random_bytes(32);                 // 32 випадкові байти
bin2hex(random_bytes(32));        // 64 hex-символи
random_int(100000, 999999);       // число в діапазоні - для кодів підтвердження
Str::random(40);                  // Laravel: на основі random_bytes
Str::password(16);                // Laravel: пароль з різними класами символів

PHP 8.2 також має ООП-API Random\Randomizer з рушієм Random\Engine\Secure (за замовчуванням).

Де потрібен CSPRNG: токени сесій і API, токени скидання пароля й підтвердження email, коди 2FA й одноразові паролі, CSRF-токени, ключі шифрування, солі (зазвичай генерує password_hash), ідентифікатори, що служать «секретом» (неперебірні посилання на документи).

Розмір має значення: 128 біт ентропії (16 байтів) - мінімум для токенів, що мають бути неперебірними; 256 біт - з запасом. Шестизначний код - лише ~20 біт, тож його захищає не випадковість, а обмеження спроб і короткий термін дії.

Пастки:

  • md5(time()), sha1(uniqid()), md5(rand()) - хешування не додає ентропії: передбачуване значення лишається передбачуваним;
  • зменшення діапазону через % (random_bytes + % 10) дає нерівномірний розподіл - для чисел використовувати random_int;
  • UUID як секрет: UUIDv4 має 122 випадкові біти і для більшості цілей годиться, якщо генерується CSPRNG. Але UUIDv7 (Laravel HasUuids, Str::uuid7()) містить мітку часу і менше випадкових бітів - як ідентифікатор він добрий, а як секретний токен - ні;
  • mt_srand() з фіксованим зерном у коді безпеки - повна передбачуваність.

У JavaScript для безпеки - crypto.getRandomValues() і crypto.randomUUID(), а не Math.random().

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