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

Питання на співбесіді з Безпека

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

103 питань

OWASP Top 10 - перелік найкритичніших категорій ризиків вебзастосунків від некомерційної спільноти OWASP. Він складається з даних про реальні вразливості й опитування спеціалістів і оновлюється раз на кілька років. Це не стандарт і не чекліст для повної перевірки, а спільна мова й відправна точка для навчання та пріоритизації.

OWASP Top 10:2025:

  1. A01 Broken Access Control - порушення контролю доступу (чужі дані за зміненим id, відсутні перевірки прав);
  2. A02 Security Misconfiguration - помилки конфігурації (налагодження на продакшені, відкриті службові файли, стандартні паролі);
  3. A03 Software Supply Chain Failures - збої ланцюжка постачання ПЗ (вразливі й скомпрометовані залежності, збірка, CI);
  4. A04 Cryptographic Failures - криптографічні помилки (незашифровані дані, слабкі алгоритми, погане керування ключами);
  5. A05 Injection - ін'єкції (SQL, команди ОС, XSS);
  6. A06 Insecure Design - небезпечний дизайн (вразливості в самій архітектурі й логіці);
  7. A07 Authentication Failures - збої автентифікації;
  8. A08 Software or Data Integrity Failures - порушення цілісності ПЗ і даних;
  9. A09 Security Logging and Alerting Failures - збої журналювання й сповіщень;
  10. A10 Mishandling of Exceptional Conditions - неправильна обробка виняткових ситуацій.

Що помітно змінилося порівняно з 2021:

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

Як використовувати на практиці:

  • як основу навчання команди й пунктів рев'ю коду;
  • для пріоритизації: контроль доступу - на першому місці не випадково, його варто перевіряти в кожному ендпойнті;
  • для повнішої перевірки - OWASP ASVS (стандарт вимог до перевірки безпеки), а не лише Top 10.

Докладніше в документації: OWASP Top 10:2025

security.txt (RFC 9116) - текстовий файл за адресою /.well-known/security.txt, що повідомляє дослідникам безпеки, як повідомити про знайдену вразливість.

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

Приклад:

Contact: mailto:security@example.com
Contact: https://example.com/security/report
Expires: 2027-10-01T00:00:00.000Z
Preferred-Languages: uk, en
Policy: https://example.com/security/policy
Acknowledgments: https://example.com/security/thanks
Canonical: https://example.com/.well-known/security.txt
Encryption: https://example.com/pgp-key.txt

Поля:

  • Contact (обов'язкове) - email, форма чи телефон для повідомлень; можна кілька;
  • Expires (обов'язкове) - до якої дати інформація актуальна. RFC радить не більше ніж на рік уперед - щоб застарілий файл з неробочими контактами не вводив в оману;
  • Policy - посилання на політику розкриття вразливостей: що досліджувати можна, а що ні, як ви реагуєте;
  • Preferred-Languages, Acknowledgments, Encryption (ключ для зашифрованих повідомлень), Canonical - необов'язкові.

Практичні моменти:

  • файл має бути доступний за HTTPS саме за шляхом /.well-known/security.txt. Правила вебсервера чи WAF, що блокують «приховані» шляхи, мають робити виняток для /.well-known/;
  • у Laravel - статичний файл public/.well-known/security.txt або маршрут, що генерує його з актуальною датою Expires;
  • адреса Contact має реально читатися - скринька, яку ніхто не перевіряє, гірша за її відсутність;
  • файл можна підписати OpenPGP, щоб підтвердити автентичність;
  • нагадування про оновлення Expires - інакше файл «протухне» через рік.

Це частина ширшої картини - політики розкриття вразливостей (VDP): security.txt лише вказує, куди писати, а процес реагування має існувати насправді.

Докладніше в документації: RFC 9116: security.txt

CVE (Common Vulnerabilities and Exposures) - унікальний ідентифікатор публічно відомої вразливості: CVE-2025-12345. Він дає змогу однозначно говорити про ту саму проблему в базах даних, звітах сканерів, бюлетенях вендорів. Інші екосистеми мають свої ідентифікатори: GHSA-... (GitHub Advisory Database), PKSA-... (Packagist).

CVSS (Common Vulnerability Scoring System) - оцінка серйозності від 0 до 10:

Бал Рівень
0.1-3.9 низький
4.0-6.9 середній
7.0-8.9 високий
9.0-10.0 критичний

Оцінка складається з метрик: вектор атаки (мережа чи потрібен локальний доступ), складність, потрібні привілеї, участь користувача, вплив на конфіденційність, цілісність і доступність. Актуальна версія стандарту - CVSS 4.0.

Як читати повідомлення (advisory) у залежності:

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

CVSS - не пріоритет у вашому контексті. «Критична» вразливість у бібліотеці, що використовується лише в CLI-скрипті розробника, може бути менш терміновою, ніж «середня» в публічному ендпойнті оплати. Бал - відправна точка, оцінка ризику - ваша.

Інструменти: composer audit, npm audit, Dependabot alerts, Snyk - зіставляють залежності з базами вразливостей. Важливо мати процес: хто розбирає сповіщення, у які терміни виправляються критичні й високі, як фіксується рішення «не стосується нас».

Докладніше в документації: FIRST: Common Vulnerability Scoring System

composer audit перевіряє встановлені PHP-пакети за базами відомих вразливостей (GitHub Advisory Database, Packagist) і повідомляє про уражені версії.

composer audit                 # за встановленими пакетами
composer audit --locked        # за composer.lock - зручно в CI до встановлення
composer audit --no-dev        # лише залежності продакшену
composer audit --format=json   # для автоматичної обробки

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

npm audit - те саме для JavaScript-залежностей фронтенду:

npm audit --omit=dev           # лише залежності, що потрапляють у збірку
npm audit --audit-level=high   # падати лише на high і critical

Як вбудувати в процес:

  1. CI на кожен пул-реквест і за розкладом. Нові вразливості публікуються щодня - проєкт без змін теж може стати вразливим, тож перевірка за розкладом (щодня чи щотижня) обов'язкова;
  2. автоматичні PR оновлень - Dependabot чи Renovate створюють пул-реквести з оновленнями й security-сповіщеннями;
  3. блокування вразливих версій - сучасний Composer за замовчуванням не дає встановити через update/require версію пакета з відомою вразливістю (налаштовується в config.policy, у старіших версіях - через ключі audit);
  4. винятки - явні й з причиною: якщо вразливість не стосується проєкту, її ігнорують з поясненням і, бажано, терміном перегляду, а не вимикають перевірку повністю:
"config": {
    "policy": {
        "advisories": {
            "ignore-id": { "CVE-2026-0001": "Не використовуємо XML-парсер пакета" }
        }
    }
}

Типові помилки:

  • запуск лише вручну «колись» - фактично ніколи;
  • ігнорування npm audit, бо «там завжди щось є». Багато знахідок - у залежностях інструментів збирання (devDependencies), що не потрапляють у браузер; --omit=dev відфільтровує шум і показує те, що справді важливо;
  • оновлення лише composer.json без composer.lock - на продакшен потрапляє версія із замка.

Пам'ятати: аудит знаходить лише відомі вразливості відомих пакетів - це не захист від скомпрометованого пакета з нульового дня.

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

Персональні дані - будь-яка інформація, за якою можна ідентифікувати людину: ім'я, email, телефон, IP-адреса, ідентифікатор пристрою, геолокація, історія замовлень. Принципи GDPR (стаття 5), на які орієнтуються й українське законодавство, і більшість сучасних регуляцій:

1. Мінімізація даних - збирати лише те, що потрібно для конкретної мети. Не просити дату народження, якщо достатньо підтвердити вік; не зберігати повний номер картки, якщо потрібні останні 4 цифри.

2. Обмеження мети - дані, зібрані для доставки замовлення, не використовуються для розсилки без окремої згоди.

3. Обмеження зберігання - дані зберігаються не довше, ніж потрібно. Неактивні облікові записи, старі журнали з IP-адресами, логи запитів - видаляються чи анонімізуються за розкладом (у Laravel - Prunable/MassPrunable моделі й запланована model:prune).

4. Точність - користувач може виправити свої дані.

5. Цілісність і конфіденційність - захист від несанкціонованого доступу: шифрування, контроль доступу, журнали дій.

6. Законність і прозорість - зрозуміла політика конфіденційності, правова підстава обробки (згода, договір, законний інтерес).

Що це означає в коді:

  • права користувача: експорт своїх даних, видалення облікового запису (з даними, що за ним стоять, - включно з резервними копіями за розумний час), відкликання згоди;
  • персональні дані не потрапляють у журнали й системи моніторингу без потреби (маскування email, телефонів, токенів);
  • розділення доступу: не всі співробітники бачать усі дані; адмінка з журналом переглядів чутливих записів;
  • шифрування особливо чутливих полів (документи, медичні дані) - каст encrypted у Laravel;
  • тестові дані: не копіювати продакшен-базу з реальними людьми на ноутбуки розробників без анонімізації;
  • сторонні сервіси (аналітика, CRM, LLM) отримують лише потрібне, з договором обробки даних.

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

Докладніше в документації: GDPR, стаття 5: принципи обробки

CSRF (Cross-Site Request Forgery) - чужий сайт змушує браузер користувача надіслати запит на ваш сайт. Браузер автоматично додає cookie вашого сайту, тож запит виглядає як справжній запит залогіненого користувача.

<!-- на evil.example -->
<form action="https://bank.example/transfer" method="POST">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>

Нападник не бачить відповіді, але дія виконується.

Захист 1 - CSRF-токен. Сервер кладе в сесію випадковий токен і вимагає його в кожному запиті, що змінює стан. Чужий сайт не може прочитати токен (same-origin policy), тож не може його підставити.

<form method="POST" action="/transfer">
    @csrf
</form>

Laravel перевіряє токен автоматично для всіх POST/PUT/PATCH/DELETE у групі web.

Захист 2 - атрибут cookie SameSite:

  • Lax (його ставить Laravel; браузери на Chromium застосовують його й до cookie без атрибута) - cookie не надсилається в міжсайтових POST-запитах, лише при звичайних переходах за посиланнями (GET).
  • Strict - не надсилається в жодних міжсайтових запитах.

Чому потрібні обидва: SameSite не захищає від запитів з піддоменів того самого сайту, а старі браузери його не підтримують. Токен - основний захист, SameSite - додатковий рівень.

Важливо: GET-запити не мають змінювати стан. GET /logout чи GET /delete?id=5 легко викликати картинкою на чужій сторінці.

API з токенами в заголовку Authorization (не в cookie) до CSRF не вразливі: браузер не додає такий заголовок автоматично.

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

SSRF (Server-Side Request Forgery) - нападник змушує ваш сервер зробити запит туди, куди йому самому не дістатися: до внутрішніх сервісів, адмінок без авторизації, метаданих хмари.

Типові функції-мішені: імпорт аватара за URL, попередній перегляд посилання, вебхуки з адресою від користувача, конвертація HTML у PDF.

// Вразливо: адресу задає користувач
$image = Http::get($request->input('avatar_url'))->body();
// avatar_url = http://169.254.169.254/latest/meta-data/iam/security-credentials/
// → сервер віддасть тимчасові ключі хмари

Інші цілі: http://localhost:6379 (Redis без пароля), http://10.0.0.5:8080/admin, file:///etc/passwd у бібліотеках, що підтримують інші схеми.

Захист:

  • Білий список, якщо можливо: дозволені лише конкретні домени.
  • Лише http/https, жодних file:, gopher:, ftp:.
  • Перевірка IP після резолву DNS: відхиляти приватні діапазони (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), 127.0.0.0/8, 169.254.0.0/16, IPv6-аналоги. Перевіряти саме той IP, до якого піде з'єднання, - інакше обхід через DNS rebinding (домен спершу резолвиться в публічну адресу, а при з'єднанні - у внутрішню).
  • Редиректи: вимкнути або перевіряти кожен, бо https://evil.example може перенаправити на http://127.0.0.1.
  • Мережева ізоляція: вихідні запити через окремий проксі чи сервіс без доступу до внутрішньої мережі.
  • У хмарі - IMDSv2 на AWS (вимагає токен, який SSRF зазвичай не отримає).

Таймаути й ліміт розміру відповіді - теж обов'язкові, інакше функцію можна використати для навантаження на сервер.

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

SSTI (server-side template injection) - дані користувача потрапляють у сам шаблон, а не в змінну, яку шаблон виводить. Рушій шаблонів компілює їх як код.

// вразливо: текст від користувача стає шаблоном
return Blade::render($request->input('greeting'), ['user' => $user]);
// greeting = "{{ system('cat .env') }}"  - або @php ... @endphp

Blade компілюється в PHP, тож SSTI в Blade - це виконання довільного PHP-коду на сервері.

Різниця, яку треба розуміти:

// безпечно: шаблон - ваш, дані - змінна, екранована {{ }}
return view('emails.welcome', ['greeting' => $request->input('greeting')]);

// небезпечно: дані користувача - частина шаблону
Blade::render($userTemplate, $data);

Де таке трапляється на практиці:

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

Як робити безпечно:

  • користувацьким шаблонам - лише заміна плейсхолдерів, а не повноцінний рушій:
$text = strtr($template, [
    '{name}' => e($user->name),
    '{order}' => e($order->number),
]);
  • якщо потрібна логіка (умови, цикли) - рушій з пісочницею і білим списком дозволених функцій (наприклад, Twig Sandbox), обмежений набір змінних, без доступу до об'єктів з методами;
  • Markdown замість HTML/Blade для контенту від користувачів - і рендер із санітизацією;
  • ніколи Blade::render, eval, create_function-подібних механізмів з даними ззовні.

Схоже на SSTI на клієнті: вираз {{ }} у даних, які Vue чи Angular компілюють як шаблон на сторінці (Vue змонтований на розмітку з користувацьким вмістом), - client-side template injection, що дає XSS.

Докладніше в документації: PortSwigger: Server-side template injection

XXE (XML External Entity) - атака через XML з визначенням зовнішньої сутності, яка вказує на файл чи URL. Парсер, що підставляє такі сутності, вставляє в документ вміст файлу сервера чи відповідь внутрішнього сервісу.

<?xml version="1.0"?>
<!DOCTYPE order [
  <!ENTITY secret SYSTEM "file:///var/www/app/.env">
]>
<order><comment>&secret;</comment></order>

Якщо застосунок потім показує чи зберігає comment - зловмисник отримує вміст .env. Варіанти - SSRF через http:// у сутності, «сліпий» XXE з передачею даних на зовнішній сервер, DoS «мільярдом сміхів» (експоненційно вкладені сутності).

Де трапляється XML: імпорт прайсів і фідів, SOAP-інтеграції, SAML (вхід через корпоративний SSO), формати Office (DOCX, XLSX - це zip з XML), SVG.

Сучасний PHP за замовчуванням безпечніший. З libxml 2.9 зовнішні сутності не підставляються, якщо їх не ввімкнути явно. Небезпечні - прапорці:

$doc = new DOMDocument();
$doc->loadXML($xml);                     // за замовчуванням сутності не розкриваються
$doc->loadXML($xml, LIBXML_NOENT);       // НЕБЕЗПЕЧНО: розкриває сутності, зокрема зовнішні
$doc->loadXML($xml, LIBXML_DTDLOAD);     // НЕБЕЗПЕЧНО: завантажує зовнішні DTD

Назва LIBXML_NOENT вводить в оману: «no entities» означає «замінити сутності їхнім вмістом», тобто ввімкнути підстановку. Його часто додають «щоб працювали &amp;-подібні сутності» - і відкривають XXE.

Функцію libxml_disable_entity_loader() оголошено застарілою в PHP 8.0 - вона більше не потрібна для захисту за замовчуванням.

Правила:

  • не використовувати LIBXML_NOENT і LIBXML_DTDLOAD з даними ззовні;
  • відхиляти документи з <!DOCTYPE>, якщо формату він не потрібен;
  • обмежувати розмір XML і глибину вкладеності;
  • бібліотеки для SAML і SOAP - актуальні версії: XXE в них - відомий клас вразливостей;
  • SVG від користувачів - окрема тема: окрім XXE під час обробки на сервері, SVG може містити скрипти (XSS при відкритті).

JSON замість XML, де це можливо, - менше поверхня атаки: у JSON немає сутностей і DTD.

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

У JavaScript об'єкти успадковують властивості від прототипу, і всі звичайні об'єкти мають спільний Object.prototype. Забруднення прототипу - зловмисник через дані змушує код записати властивість у сам прототип, і вона з'являється в усіх об'єктах застосунку.

Як це трапляється - рекурсивне злиття чи встановлення властивості за шляхом з даних користувача:

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      target[key] ??= {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));

({}).isAdmin;   // true - у кожного об'єкта

Ключ __proto__ вказує на прототип, тож запис іде в Object.prototype. Так само через constructor.prototype.

Наслідки:

  • обхід перевірок: if (user.isAdmin) для об'єкта, в якому поля немає, раптом дає true;
  • XSS у браузері: бібліотека читає налаштування з об'єкта опцій (options.innerHTML, options.src), а «забруднене» значення підставляється в DOM. Так атакують через параметри URL (?__proto__[x]=...) з бібліотеками, що розбирають рядок запиту у вкладені об'єкти;
  • виконання коду на сервері (Node.js): забруднені опції child_process, рушіїв шаблонів;
  • DoS - зламана логіка всього застосунку.

Захист:

  • блокувати небезпечні ключі при злитті й записі за шляхом: __proto__, constructor, prototype;
  • об'єкти без прототипу для словників з даних: Object.create(null);
  • Map замість звичайного об'єкта для довільних ключів;
  • валідація схемою (Zod) з «суворими» об'єктами - невідомі ключі відкидаються;
  • оновлювати бібліотеки злиття, розбору рядка запиту, глибокого копіювання - багато відомих CVE саме тут (lodash merge, qs, jQuery extend);
  • Object.freeze(Object.prototype) - радикальний захист, але може зламати сторонні бібліотеки.

Чим це стосується PHP-розробника: фронтенд Laravel-застосунку (Vue, React, Alpine) і Node-скрипти збирання/SSR - теж частина поверхні атаки; оновлення npm-залежностей важливе так само, як composer.

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

Сесія: сервер зберігає стан (в базі, 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: керування сесіями

Питання з реальних технічних співбесід - 103 питання у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 35 Middle 37 Senior 31

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії