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

Junior: питання на співбесіді з теми «Автентифікація й секрети»

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

5 питань

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

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

Чому не 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: шпаргалка з автентифікації