Питання на співбесіді з Безпека
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
103 питань
OWASP Top 10 - перелік найкритичніших категорій ризиків вебзастосунків від некомерційної спільноти OWASP. Він складається з даних про реальні вразливості й опитування спеціалістів і оновлюється раз на кілька років. Це не стандарт і не чекліст для повної перевірки, а спільна мова й відправна точка для навчання та пріоритизації.
OWASP Top 10:2025:
- A01 Broken Access Control - порушення контролю доступу (чужі дані за зміненим id, відсутні перевірки прав);
- A02 Security Misconfiguration - помилки конфігурації (налагодження на продакшені, відкриті службові файли, стандартні паролі);
- A03 Software Supply Chain Failures - збої ланцюжка постачання ПЗ (вразливі й скомпрометовані залежності, збірка, CI);
- A04 Cryptographic Failures - криптографічні помилки (незашифровані дані, слабкі алгоритми, погане керування ключами);
- A05 Injection - ін'єкції (SQL, команди ОС, XSS);
- A06 Insecure Design - небезпечний дизайн (вразливості в самій архітектурі й логіці);
- A07 Authentication Failures - збої автентифікації;
- A08 Software or Data Integrity Failures - порушення цілісності ПЗ і даних;
- A09 Security Logging and Alerting Failures - збої журналювання й сповіщень;
- A10 Mishandling of Exceptional Conditions - неправильна обробка виняткових ситуацій.
Що помітно змінилося порівняно з 2021:
- ланцюжок постачання став окремою широкою категорією (замість вужчої «вразливі й застарілі компоненти»);
- неправильна конфігурація піднялася на друге місце;
- SSRF більше не окрема категорія - увійшла до контролю доступу;
- з'явилася нова категорія про обробку виняткових ситуацій: помилки, що «відкривають» доступ (fail-open), витоки через повідомлення про помилки;
- у категорії журналювання акцент на сповіщеннях: недостатньо писати журнали, хтось має реагувати.
Як використовувати на практиці:
- як основу навчання команди й пунктів рев'ю коду;
- для пріоритизації: контроль доступу - на першому місці не випадково, його варто перевіряти в кожному ендпойнті;
- для повнішої перевірки - OWASP ASVS (стандарт вимог до перевірки безпеки), а не лише Top 10.
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 лише вказує, куди писати, а процес реагування має існувати насправді.
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) у залежності:
- чи вразлива наша версія? Діапазон уражених версій і версія з виправленням;
- чи використовуємо ми вразливу частину? Вразливість у функції завантаження XML не загрожує, якщо застосунок її не викликає. Але «не використовуємо» треба перевірити, а не припустити;
- чи досяжна вона ззовні? Вектор «мережа, без автентифікації» в публічному застосунку - найгірший випадок;
- чи є публічний експлойт або ознаки активної експлуатації (каталог CISA KEV) - це різко підвищує терміновість;
- що потрібно зробити: оновити, застосувати обхідний шлях, тимчасове правило 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
Як вбудувати в процес:
- CI на кожен пул-реквест і за розкладом. Нові вразливості публікуються щодня - проєкт без змін теж може стати вразливим, тож перевірка за розкладом (щодня чи щотижня) обов'язкова;
- автоматичні PR оновлень - Dependabot чи Renovate створюють пул-реквести з оновленнями й security-сповіщеннями;
- блокування вразливих версій - сучасний Composer за замовчуванням не дає встановити через
update/requireверсію пакета з відомою вразливістю (налаштовується вconfig.policy, у старіших версіях - через ключіaudit); - винятки - явні й з причиною: якщо вразливість не стосується проєкту, її ігнорують з поясненням і, бажано, терміном перегляду, а не вимикають перевірку повністю:
"config": {
"policy": {
"advisories": {
"ignore-id": { "CVE-2026-0001": "Не використовуємо XML-парсер пакета" }
}
}
}
Типові помилки:
- запуск лише вручну «колись» - фактично ніколи;
- ігнорування
npm audit, бо «там завжди щось є». Багато знахідок - у залежностях інструментів збирання (devDependencies), що не потрапляють у браузер;--omit=devвідфільтровує шум і показує те, що справді важливо; - оновлення лише
composer.jsonбезcomposer.lock- на продакшен потрапляє версія із замка.
Пам'ятати: аудит знаходить лише відомі вразливості відомих пакетів - це не захист від скомпрометованого пакета з нульового дня.
Персональні дані - будь-яка інформація, за якою можна ідентифікувати людину: ім'я, email, телефон, IP-адреса, ідентифікатор пристрою, геолокація, історія замовлень. Принципи GDPR (стаття 5), на які орієнтуються й українське законодавство, і більшість сучасних регуляцій:
1. Мінімізація даних - збирати лише те, що потрібно для конкретної мети. Не просити дату народження, якщо достатньо підтвердити вік; не зберігати повний номер картки, якщо потрібні останні 4 цифри.
2. Обмеження мети - дані, зібрані для доставки замовлення, не використовуються для розсилки без окремої згоди.
3. Обмеження зберігання - дані зберігаються не довше, ніж потрібно. Неактивні облікові записи, старі журнали з IP-адресами, логи запитів - видаляються чи анонімізуються за розкладом (у Laravel - Prunable/MassPrunable моделі й запланована model:prune).
4. Точність - користувач може виправити свої дані.
5. Цілісність і конфіденційність - захист від несанкціонованого доступу: шифрування, контроль доступу, журнали дій.
6. Законність і прозорість - зрозуміла політика конфіденційності, правова підстава обробки (згода, договір, законний інтерес).
Що це означає в коді:
- права користувача: експорт своїх даних, видалення облікового запису (з даними, що за ним стоять, - включно з резервними копіями за розумний час), відкликання згоди;
- персональні дані не потрапляють у журнали й системи моніторингу без потреби (маскування email, телефонів, токенів);
- розділення доступу: не всі співробітники бачать усі дані; адмінка з журналом переглядів чутливих записів;
- шифрування особливо чутливих полів (документи, медичні дані) - каст
encryptedу Laravel; - тестові дані: не копіювати продакшен-базу з реальними людьми на ноутбуки розробників без анонімізації;
- сторонні сервіси (аналітика, CRM, LLM) отримують лише потрібне, з договором обробки даних.
Витік персональних даних - юридична подія: GDPR вимагає повідомити наглядовий орган протягом 72 годин після виявлення, якщо витік несе ризик для людей. План дій має бути готовий заздалегідь.
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 не вразливі: браузер не додає такий заголовок автоматично.
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 зазвичай не отримає).
Таймаути й ліміт розміру відповіді - теж обов'язкові, інакше функцію можна використати для навантаження на сервер.
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» означає «замінити сутності їхнім вмістом», тобто ввімкнути підстановку. Його часто додають «щоб працювали &-подібні сутності» - і відкривають 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.
У 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, jQueryextend); Object.freeze(Object.prototype)- радикальний захист, але може зламати сторонні бібліотеки.
Чим це стосується PHP-розробника: фронтенд Laravel-застосунку (Vue, React, Alpine) і Node-скрипти збирання/SSR - теж частина поверхні атаки; оновлення npm-залежностей важливе так само, як composer.
Сесія: сервер зберігає стан (в базі, 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, не класти в токен секретних даних.
Фіксація сесії (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, видаленням облікового запису.
Скидання пароля - «чорний хід» в обліковий запис. Якщо він слабший за вхід, атакують саме його.
Вимоги до токена скидання:
- випадковий і довгий - криптографічно стійкий генератор (
random_bytes,Str::random()), не похідний від email, часу чи id; - одноразовий - після використання недійсний;
- короткий термін дії - хвилини чи година;
- у базі - хеш токена, а не сам токен: витік бази не дає активних посилань для скидання;
- прив'язаний до облікового запису - токен одного користувача не працює для іншого.
Процес:
- користувач вводить email - відповідь однакова незалежно від того, чи існує обліковий запис («Якщо обліковий запис існує, ми надіслали лист»);
- лист із посиланням, що містить токен; відправка - через чергу (щоб час відповіді не видавав існування облікового запису);
- перехід за посиланням - форма нового пароля; токен перевіряється порівнянням хешів у постійному часі;
- новий пароль - з тими самими правилами, що й при реєстрації (довжина, перевірка у базі витоків);
- після зміни: токен знищено, усі інші сесії й токени 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.
Passkey - облікові дані на основі криптографії з відкритим ключем (стандарт WebAuthn/FIDO2) замість пароля.
Як це працює:
- реєстрація: пристрій користувача (телефон, ноутбук, апаратний ключ) створює пару ключів для конкретного сайту. Приватний ключ лишається на пристрої (в захищеному сховищі), сайт отримує й зберігає лише публічний;
- вхід: сервер надсилає випадковий виклик (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) уже пропонують вхід без пароля.
«Запам'ятати мене» - користувач лишається автентифікованим тижні й місяці, навіть після закриття браузера, коли звичайна сесія вже завершилася.
Як це роблять правильно:
- окремий довгоживучий токен у cookie - випадковий, криптографічно стійкий, а не email, id чи хеш пароля;
- у базі - хеш токена (чи токен, прив'язаний до користувача, як у Laravel -
remember_token), щоб витік бази не давав готових cookie; - cookie з
HttpOnly,Secure,SameSite- недоступна JavaScript і не передається по HTTP; - при використанні токен створює нову звичайну сесію;
- при виході токен знищується на сервері, а не лише стирається cookie в браузері.
Ризики довгих сесій і що з ними робити:
- викрадена cookie діє довго. Обмежити абсолютний термін (наприклад, 30 днів), а для чутливих дій вимагати повторного підтвердження паролем (зміна email, пароля, MFA, платіжних даних) - так працює
password.confirmmiddleware в Laravel; - вихід «з усіх пристроїв» має справді анулювати remember-токени. У Laravel зміна
remember_token(наприклад, при зміні пароля чиAuth::logoutOtherDevices()) робить старі cookie недійсними; - «ротація» токена при кожному використанні з виявленням повторного використання - так помічають, що cookie скопійовано;
- список активних сесій у налаштуваннях облікового запису з пристроєм, місцем і часом останньої активності - користувач бачить чужий вхід і може його завершити (драйвер сесій
databaseдає таку можливість).
Таймаути сесій (рекомендації OWASP):
- неактивність - сесія завершується після N хвилин без дій (для банківських застосунків - хвилини, для звичайних сайтів - години);
- абсолютний - навіть активна сесія закінчується після певного часу;
- для адмінок і фінансових операцій - коротші терміни, ніж для звичайних користувачів.
Регенерація ідентифікатора сесії після входу, зміни прав і виходу - захист від фіксації сесії ($request->session()->regenerate()).
Компроміс: «Запам'ятати мене» - зручність коштом безпеки. Для публічних і спільних комп'ютерів його не варто вмикати за замовчуванням; для застосунків з чутливими даними варто обмежити або вимагати MFA при відновленні сесії з remember-токена.
Питання з реальних технічних співбесід - 103 питання у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії