Senior: питання на співбесіді з теми «Безпека Laravel-застосунку»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
APP_KEY часто сприймають як «ще одну змінну в .env», хоча його витік - один з найсерйозніших інцидентів для Laravel-застосунку.
Що дає ключ зловмиснику:
1. Розшифрування даних. Усе, що зашифровано Crypt чи кастами encrypted: токени сторонніх API, секрети 2FA, персональні дані - якщо зловмисник має й дамп бази.
2. Підробка cookie й сесій. Cookie Laravel шифруються й підписуються ключем. З ключем можна створити валідну зашифровану cookie з довільним вмістом. Наслідки залежать від конфігурації:
- з драйвером сесій
cookieсесія повністю зберігається в cookie - зловмисник створює сесію будь-якого користувача; - історично Laravel серіалізував вміст cookie, і підроблена cookie з об'єктом давала виконання коду на сервері через небезпечну десеріалізацію. У сучасних версіях cookie за замовчуванням не серіалізуються, але застосунки зі старими налаштуваннями чи сторонні механізми, що десеріалізують розшифровані дані, лишаються вразливими.
3. Підробка підписаних URL: підтвердження email для чужих адрес, «чарівні посилання» входу, одноразові посилання на файли, відписки - усе, що захищено signed.
4. Підробка даних сторонніх механізмів, що підписують дані ключем застосунку: наприклад, знімки стану Livewire - контрольна сума перестає захищати від зміни стану компонента.
Як ключі витікають:
.envу репозиторії (включно з історією Git), у Docker-образі, в архіві бекапу;.envдоступний з вебу через неправильний корінь вебсервера;- ключ з прикладів і туторіалів, який скопіювали в продакшен;
- сторінка налагодження чи
phpinfo()з змінними оточення; - логи CI/CD, що вивели змінні оточення.
Реагування на витік:
- новий
APP_KEY; старий - не вAPP_PREVIOUS_KEYS(інакше підроблені дані й далі розшифровуватимуться), тож свідомо прийняти, що всі сесії й посилання стануть недійсними; - перешифрувати дані в базі (потрібен старий ключ для читання - тимчасово в окремому скрипті);
- завершити всі сесії, відкликати токени;
- розслідувати, як ключ витік, і перевірити логи на ознаки використання;
- ротувати й інші секрети з того самого
.env- найімовірніше, витік не обмежився одним ключем.
Профілактика: секрети поза репозиторієм і образами (змінні оточення, менеджер секретів, env:encrypt), корінь вебсервера на public/, сканування репозиторіїв на секрети.
Довірені проксі. Якщо перед застосунком стоїть балансувальник чи CDN (Cloudflare, AWS ALB, Nginx), справжні IP клієнта, протокол і хост приходять у заголовках X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host. Laravel використовує їх, лише якщо проксі довірений:
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: ['10.0.0.0/8']); // адреси своїх балансувальників
// $middleware->trustProxies(at: '*'); // лише якщо застосунок недоступний напряму
})
Без налаштування: $request->ip() повертає IP балансувальника - ліміти частоти діють на всіх клієнтів разом, логи марні; $request->secure() - false, і генеруються http:// посилання.
З at: '*', коли застосунок доступний і напряму: будь-хто надсилає X-Forwarded-For: 1.2.3.4 - і обходить ліміти частоти за IP, підробляє адреси в журналах аудиту.
Довірені хости. TrustHosts у Laravel за замовчуванням вимкнений - застосунок приймає запити з будь-яким заголовком Host, якщо вебсервер їх пропускає.
Отруєння посилання скидання пароля (host header injection):
- зловмисник надсилає запит на скидання пароля для email жертви з підробленим заголовком
Host: evil.example; - якщо URL у листі будується з хоста поточного запиту, жертва отримує справжній лист від вашого сервісу з посиланням
https://evil.example/reset-password/{token}; - жертва переходить - токен потрапляє до зловмисника, і він скидає пароль.
Те саме з будь-якими абсолютними посиланнями, згенерованими з запиту: підтвердження email, запрошення, посилання в кешованих сторінках (отруєння кешу).
Захист:
$middleware->trustHosts(at: ['laravelukraine.com'], subdomains: true);
trustHosts()- запити з іншимHostвідхиляються;- вебсервер приймає лише свої домени (без «default server», що обслуговує будь-який хост);
- посилання в листах і фонових задачах - з
APP_URL(URL::forceRootUrl(config('app.url'))для черг і консолі, де запиту немає); URL::forceScheme('https')на продакшені за проксі, якщо схему не можна надійно визначити.
Перевірка: curl -H 'Host: evil.example' https://ваш-сайт/forgot-password - відповідь має бути помилкою, а не нормальною сторінкою.
Налаштування в config/session.php (і змінні SESSION_* у .env) безпосередньо визначають, наскільки легко викрасти чи підробити сесію.
Атрибути cookie:
| Параметр | Безпечне значення | Навіщо |
|---|---|---|
secure (SESSION_SECURE_COOKIE) |
true на продакшені |
cookie лише через HTTPS |
http_only |
true (за замовчуванням) |
недоступна JavaScript - XSS не вкраде сесію |
same_site |
lax (за замовчуванням) чи strict |
не надсилається в міжсайтових POST - захист від CSRF |
domain |
null чи конкретний домен |
cookie для .example.com бачать усі піддомени, включно з тими, що можуть бути скомпрометовані |
partitioned |
для вбудованих сторінок у сторонніх сайтах | CHIPS - окреме сховище для кожного сайту-власника |
SESSION_SECURE_COOKIE за замовчуванням не задано - Laravel не виставляє Secure, доки ви не ввімкнете. На продакшені з HTTPS - обов'язково true.
Драйвер сесій:
database,redis- дані на сервері, у cookie лише ідентифікатор. Можна переглянути й завершити сесії користувача («вийти з усіх пристроїв»);cookie- усі дані сесії в зашифрованій cookie. Неможливо примусово завершити сесію на сервері, розмір обмежений, а витікAPP_KEYдає підробку сесій;file- на кількох серверах не працює без спільного сховища.
encrypt (SESSION_ENCRYPT) - шифрувати дані сесії у сховищі. Корисно, якщо в сесії бувають чутливі дані, а доступ до Redis чи таблиці сесій мають інші системи.
Терміни:
lifetime- хвилини неактивності до завершення сесії (за замовчуванням 120);expire_on_close- сесія закінчується при закритті браузера;- для адмінок і фінансових застосунків - коротші терміни й повторне підтвердження пароля для чутливих дій.
Регенерація ідентифікатора:
- після входу -
$request->session()->regenerate()(стандартні контролери автентифікації роблять це) - захист від фіксації сесії; - при виході -
invalidate()іregenerateToken().
Що ще перевірити:
- окремі назви cookie (
SESSION_COOKIE) для різних застосунків на одному домені - інакше вони перезаписують сесії одне одного; SameSite=None(потрібна для вбудовування в iframe на інших сайтах) - лише зSecureі з усвідомленням, що захист від CSRF тепер повністю на токенах;- таблиця сесій містить IP і user agent - персональні дані з відповідним терміном зберігання.
Laravel хешує паролі через Hash::make() (і каст hashed у моделі User). Налаштування - у config/hashing.php.
Алгоритм за замовчуванням - bcrypt з вартістю (BCRYPT_ROUNDS) 12: кожне збільшення на 1 подвоює час обчислення. Альтернатива - Argon2id (HASH_DRIVER=argon2id) з параметрами пам'яті, часу й потоків (ARGON_MEMORY, ARGON_TIME, ARGON_THREADS).
bcrypt чи Argon2id:
- Argon2id - сучасний рекомендований OWASP варіант: вимагає багато пам'яті, тому перебір на GPU й спеціалізованих чипах значно дорожчий;
- bcrypt - перевірений часом, доступний усюди, але має обмеження в 72 байти: усе, що довше, ігнорується. Пароль з 80 символів і той самий пароль з іншими останніми символами дадуть однаковий хеш. Для кирилиці (2 байти на символ) межа - ~36 символів. Laravel має параметр
BCRYPT_LIMIT, щоб відхиляти задовгі паролі замість мовчазного обрізання.
Як обирати вартість: обчислення хешу має займати приблизно сотні мілісекунд на вашому сервері. Менше - легше перебирати при витоку бази; більше - повільний вхід і можливість DoS через масові спроби входу. Вартість варто переглядати разом з обладнанням.
Повторне хешування при вході. У Laravel 11+ rehash_on_login увімкнено за замовчуванням: при успішному вході Laravel перевіряє, чи хеш створено з поточними налаштуваннями, і якщо ні - перехешовує пароль (відкритий текст на цей момент відомий). Тому підвищення BCRYPT_ROUNDS чи перехід на Argon2id відбуваються поступово, без примусового скидання паролів.
verify (HASH_VERIFY=true) - перевірка, що хеш створено очікуваним алгоритмом. Захищає від ситуацій, коли в базі опиняються хеші іншого типу (наприклад, після імпорту), що перевірялися б іншим, можливо слабшим, способом.
Ручне перехешування:
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make($plainPassword);
$user->save();
}
Типові помилки:
md5,sha1,sha256для паролів - швидкі хеші, мільярди перевірок за секунду на GPU;- шифрування паролів замість хешування;
- власна «сіль» чи комбінації алгоритмів -
password_hashі Laravel уже додають сіль автоматично; - знижена вартість у продакшені через
BCRYPT_ROUNDSз тестового.env: у тестах низька вартість доречна для швидкості (phpunit.xmlзадаєBCRYPT_ROUNDS=4), але не на сервері.