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

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, що вивели змінні оточення.

Реагування на витік:

  1. новий APP_KEY; старий - не в APP_PREVIOUS_KEYS (інакше підроблені дані й далі розшифровуватимуться), тож свідомо прийняти, що всі сесії й посилання стануть недійсними;
  2. перешифрувати дані в базі (потрібен старий ключ для читання - тимчасово в окремому скрипті);
  3. завершити всі сесії, відкликати токени;
  4. розслідувати, як ключ витік, і перевірити логи на ознаки використання;
  5. ротувати й інші секрети з того самого .env - найімовірніше, витік не обмежився одним ключем.

Профілактика: секрети поза репозиторієм і образами (змінні оточення, менеджер секретів, env:encrypt), корінь вебсервера на public/, сканування репозиторіїв на секрети.

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

Довірені проксі. Якщо перед застосунком стоїть балансувальник чи 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):

  1. зловмисник надсилає запит на скидання пароля для email жертви з підробленим заголовком Host: evil.example;
  2. якщо URL у листі будується з хоста поточного запиту, жертва отримує справжній лист від вашого сервісу з посиланням https://evil.example/reset-password/{token};
  3. жертва переходить - токен потрапляє до зловмисника, і він скидає пароль.

Те саме з будь-якими абсолютними посиланнями, згенерованими з запиту: підтвердження 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 - відповідь має бути помилкою, а не нормальною сторінкою.

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

Налаштування в 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: налаштування сесій

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), але не на сервері.

Докладніше в документації: Laravel: хешування