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

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

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

103 питань

php artisan env:encrypt шифрує файл оточення, щоб його можна було безпечно зберігати в репозиторії - разом з кодом, з історією змін і рев'ю, але без відкритих секретів.

php artisan env:encrypt --env=production
# створює .env.production.encrypted, виводить ключ (показується один раз)
  • шифр за замовчуванням - AES-256-CBC (можна змінити через --cipher);
  • ключ генерується автоматично або передається через --key;
  • --prune - видалити вихідний відкритий файл після шифрування;
  • --readable - шифрувати кожне значення окремо, залишаючи назви змінних відкритими: у diff видно, яка змінна змінилася, без розкриття значення.

Розшифрування на сервері під час деплою:

php artisan env:decrypt --env=production --key="$LARAVEL_ENV_ENCRYPTION_KEY"

Ключ береться з опції --key або змінної оточення LARAVEL_ENV_ENCRYPTION_KEY. Тобто на сервер (чи в CI) потрапляє один секрет замість десятків.

Що це дає:

  • секрети версіонуються разом з кодом - відомо, хто й коли змінив конфігурацію;
  • новий член команди отримує доступ до конфігурації оточення через один ключ;
  • немає «живих» .env, що пересилаються в месенджерах.

Обмеження й ризики:

  • увесь захист - в одному ключі. Його витік відкриває всі секрети з усієї історії репозиторію. Ключ зберігають у менеджері секретів CI чи хостингу, а не поруч з кодом;
  • ротація секретів все одно потрібна - шифрування файлу не скасовує заміну скомпрометованих значень;
  • історія: зашифровані версії лишаються в Git назавжди. Якщо ключ колись витік, старі версії з тоді чинними секретами теж розкриті;
  • доступ «все або нічого»: будь-хто з ключем бачить усі секрети оточення. Для розмежування доступу краще повноцінний менеджер секретів (Vault, AWS Secrets Manager, Doppler) чи SOPS з ключами для різних людей.

Добрий компроміс для невеликих команд: зашифрований .env.production у репозиторії, ключ - у змінних CI/хостингу, розшифрування на етапі деплою.

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

Сертифікати - найчастіше від Let's Encrypt (безкоштовні, на 90 днів) через протокол ACME. Короткий термін - свідомий вибір: компрометований сертифікат швидше перестає діяти, а автоматизація стає обов'язковою.

Автоматизація замість ручного оновлення:

  • Caddy чи FrankenPHP отримують і оновлюють сертифікати автоматично, без жодних налаштувань;
  • Certbot з таймером systemd для Nginx/Apache;
  • хмарні балансувальники й CDN (Cloudflare, AWS) керують сертифікатами самі.

Прострочений сертифікат - одна з найпоширеніших причин «падіння» сайту, тому навіть з автоматизацією потрібен моніторинг терміну дії зі сповіщенням за 2-3 тижні.

Конфігурація TLS - не вигадувати власну, а брати перевірену: Mozilla SSL Configuration Generator генерує налаштування для Nginx, Apache, HAProxy під профіль:

  • Modern - лише TLS 1.3 (сучасні клієнти);
  • Intermediate - TLS 1.2 і 1.3 з надійними шифрами (рекомендований для більшості сайтів);
  • Old - для дуже старих клієнтів (уникати).

Що має бути в налаштуваннях:

  • вимкнені SSL 3.0, TLS 1.0 і 1.1;
  • шифри з прямою секретністю (ECDHE) і AEAD (AES-GCM, ChaCha20-Poly1305);
  • HSTS (Strict-Transport-Security) після того, як HTTPS працює стабільно;
  • редирект з HTTP на HTTPS для сайту (для API - краще відмова);
  • OCSP stapling - якщо CA його підтримує (Let's Encrypt відмовляється від OCSP на користь коротших сертифікатів і CRL).

Перевірка: SSL Labs Server Test (оцінка A/A+), testssl.sh для внутрішніх серверів.

Внутрішній трафік: TLS, що закінчується на балансувальнику чи Cloudflare, лишає незашифрованим шлях до сервера застосунку. Для Cloudflare - режим Full (strict) з сертифікатом на сервері (зокрема безкоштовним Origin CA), а не Flexible, де трафік до origin іде по HTTP.

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

Докладніше в документації: Mozilla SSL Configuration Generator

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

1. Не працювати від root.

FROM php:8.5-fpm-alpine
# ...
RUN addgroup -S app && adduser -S app -G app
USER app

Вразливість у застосунку, що дає виконання коду, в контейнері від root - це root у контейнері, і значно ближче до хоста.

2. Мінімальний образ. Alpine чи slim-варіанти, без компіляторів, curl, git і пакетів «на всяк випадок». Менше програм - менше вразливостей і інструментів для зловмисника. Багатоетапне збирання (multi-stage) - залежності й ассети збираються в одному етапі, а в фінальний образ потрапляє лише результат.

3. Жодних секретів в образі. COPY .env . чи ARG API_KEY лишаються в шарах образу назавжди - будь-хто з доступом до образу їх дістане (docker history, розпакування шарів). Секрети - змінними оточення чи змонтованими файлами під час запуску; для збирання - --mount=type=secret. Плюс .dockerignore з .env, .git, node_modules.

4. Файлова система лише для читання, де можливо (read_only: true у Compose), з окремими томами для storage/ і тимчасових файлів.

5. Обмеження прав контейнера:

  • cap_drop: [ALL] і лише потрібні можливості;
  • no-new-privileges: true;
  • ніколи privileged: true і монтування /var/run/docker.sock у контейнер застосунку - це фактично root на хості.

6. Оновлення й сканування:

  • закріплені версії базових образів (а ще краще - дайджест) і регулярне перезбирання з оновленою базою;
  • сканування вразливостей образів у CI: Trivy, Grype, Docker Scout.

7. Мережа й ресурси: контейнер бази не публікує порт назовні, ліміти пам'яті й CPU (захист від виснаження ресурсів).

8. Health check і журнали в stdout - щоб оркестратор бачив стан, а журнали не накопичувалися в контейнері.

Пастка закріплення: «плаваючий» тег (php:8.5-fpm) може раптово принести несумісні зміни, а закріплений назавжди - перестати отримувати виправлення безпеки. Рішення - закріплена версія + автоматичні PR оновлень (Renovate, Dependabot).

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

Сервер, щойно отримавши публічну IP-адресу, за лічені хвилини починає отримувати спроби підбору паролів SSH і сканування портів. Базовий захист:

1. SSH:

  • вхід лише за ключами, паролі вимкнено (PasswordAuthentication no);
  • заборонено вхід root (PermitRootLogin no) - окремий користувач з sudo;
  • сучасні ключі (Ed25519), захищені парольною фразою;
  • доступ до SSH - лише з певних адрес чи через VPN/bastion (WireGuard, Tailscale), або хоча б fail2ban проти перебору.

Mozilla публікує рекомендовану конфігурацію OpenSSH з безпечними алгоритмами.

2. Фаєрвол - відкрито лише потрібне:

ufw default deny incoming
ufw allow 22/tcp        # або лише з адреси VPN
ufw allow 80,443/tcp
ufw enable

База даних, Redis, панелі моніторингу - не слухають публічний інтерфейс. Окрема пастка Docker: опубліковані порти (ports: 5432:5432) обходять ufw, бо Docker сам керує правилами iptables. Порти служб - лише на 127.0.0.1 або без публікації.

3. Оновлення: автоматичні оновлення безпеки (unattended-upgrades в Ubuntu/Debian), регулярне оновлення ядра з перезавантаженням.

4. Принцип найменших прав:

  • застосунок працює від окремого непривілейованого користувача;
  • права на файли: код - лише для читання процесом PHP, запис - лише в storage/ і bootstrap/cache/;
  • без chmod 777 «щоб запрацювало».

5. Мінімум служб: видалити непотрібне ПЗ, не встановлювати phpMyAdmin і подібні панелі на продакшені.

6. Журнали й моніторинг: спроби входу, sudo, зміни в системних файлах, незвичне навантаження. Журнали - на окремий сервер, щоб зловмисник не міг їх стерти.

7. Інфраструктура як код: налаштування через Ansible, cloud-init чи образи - відтворювано й перевірено, а не «руками колись налаштований сервер», про який ніхто не пам'ятає подробиць.

Керовані платформи (Laravel Cloud, Forge, Ploi, контейнерні платформи) беруть частину цього на себе - але відповідальність за доступи, секрети й оновлення застосунку лишається за командою.

Докладніше в документації: Mozilla: рекомендації для OpenSSH

Значення за замовчуванням у PHP розраховані на зручність розробки. Для продакшену частину треба змінити.

Не розкривати інформацію:

expose_php = Off          ; без заголовка X-Powered-By: PHP/8.5.x
display_errors = Off      ; помилки не виводяться користувачу
display_startup_errors = Off
log_errors = On           ; але записуються в журнал
error_reporting = E_ALL

Точна версія PHP у заголовку підказує зловмиснику, які відомі вразливості перевіряти. Виведені помилки розкривають шляхи файлів і фрагменти коду.

Обмежити ресурси (захист від виснаження):

memory_limit = 256M
max_execution_time = 30
max_input_time = 60
post_max_size = 20M
upload_max_filesize = 10M
max_input_vars = 1000

Вимкнути небезпечне:

allow_url_include = Off   ; include за URL - класичний шлях до виконання чужого коду
allow_url_fopen = Off     ; якщо застосунку не потрібно (HTTP-клієнти працюють через curl)

disable_functions - вимкнути функції виконання команд (exec, shell_exec, system, passthru, proc_open, popen), якщо застосунок їх не використовує. Обережно: Laravel-черги, Horizon, деякі пакети й команди artisan можуть потребувати proc_open - перевіряти, а не вимикати наосліп. Для CLI і FPM можна мати різні налаштування.

open_basedir обмежує каталоги, доступні PHP. Корисно на спільному хостингу, але це не межа безпеки (історично обходилося) і помітно сповільнює роботу з файлами. У контейнерах і на виділених серверах ізоляцію краще робити засобами ОС.

Сесії (якщо використовується нативна сесія PHP):

session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
session.cookie_samesite = Lax

Laravel має власну систему сесій з налаштуваннями в config/session.php, тож тут важливіше перевірити її.

OPcache для продакшену: opcache.validate_timestamps = 0 (швидкість), але тоді після деплою OPcache треба скидати - інакше працюватиме старий код, включно з невиправленою вразливістю.

Актуальна версія PHP - найважливіше налаштування: версії, що не отримують виправлень безпеки, мають відомі невиправлені вразливості.

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

Моделювання загроз - систематичний пошук того, що може піти не так з безпекою, на етапі проєктування, до написання коду. Виправити дизайн на дошці дешевше, ніж переписувати готову функцію чи реагувати на інцидент.

Чотири питання (за OWASP і Адамом Шостаком):

  1. Над чим ми працюємо? - модель системи;
  2. Що може піти не так? - загрози;
  3. Що ми з цим робимо? - заходи;
  4. Чи добре ми впоралися? - перевірка.

Модель системи - діаграма потоків даних (DFD): зовнішні сутності (користувач, платіжна система), процеси (застосунок, черга), сховища (база, S3), потоки даних між ними і межі довіри - місця, де дані переходять з менш довіреної зони в більш довірену (браузер → сервер, сторонній вебхук → застосунок).

STRIDE - категорії загроз для кожного елемента діаграми:

Літера Загроза Порушена властивість Приклад
S Spoofing - підробка автентичність підроблений вебхук від «платіжної системи»
T Tampering - зміна цілісність зміна ціни в запиті оформлення замовлення
R Repudiation - заперечення неспростовність «я не змінював цей платіж» - а журналу немає
I Information disclosure - розкриття конфіденційність чужий рахунок за іншим id
D Denial of service - відмова доступність експорт без ліміту, що вантажить базу
E Elevation of privilege - підвищення прав авторизація користувач робить себе адміністратором

Приклад: нова функція «імпорт товарів з CSV за посиланням»:

  • S: хто може запускати імпорт? → лише роль менеджера;
  • T: чи можна підмінити файл між перевіркою й обробкою? → завантажити один раз і обробляти локальну копію;
  • R: хто і коли імпортував? → журнал з користувачем і файлом;
  • I: чи може URL вказувати на внутрішні ресурси (SSRF)? → перевірка URL і IP;
  • D: файл на 10 ГБ? → ліміт розміру, обробка в черзі;
  • E: CSV-ін'єкція формул при експорті назад в Excel? → екранування.

Як впровадити без бюрократії: 30-60 хвилин обговорення для функцій, що зачіпають автентифікацію, платежі, файли, зовнішні інтеграції, персональні дані; результат - перелік загроз і задач у трекері, а не документ на полиці.

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

Три класи автоматизованих перевірок безпеки шукають різне і доповнюють одне одного.

SAST (Static Application Security Testing) - аналіз вихідного коду без запуску:

  • знаходить небезпечні шаблони: конкатенацію в SQL, eval, unserialize даних користувача, виведення без екранування;
  • taint-аналіз відстежує, чи потрапляють дані з запиту ($request->input()) у небезпечні місця (DB::raw, exec) без очищення;
  • інструменти для PHP: Psalm (taint-аналіз --taint-analysis), Semgrep з правилами для PHP і Laravel, SonarQube; PHPStan з пакетами правил для частини перевірок;
  • плюси: працює рано, в PR, вказує на рядок коду;
  • мінуси: хибні спрацьовування, не бачить конфігурації й поведінки під час виконання.

DAST (Dynamic Application Security Testing) - атакує запущений застосунок ззовні, як зловмисник:

  • знаходить те, що видно лише в роботі: відсутні заголовки безпеки, відкриті службові файли, XSS і ін'єкції у відповідях, проблеми TLS, налагоджувальні сторінки;
  • інструменти: OWASP ZAP (зокрема в CI в режимі baseline scan), Burp Suite, Nuclei з шаблонами для відомих вразливостей;
  • плюси: реальна поведінка, мало хибних спрацьовувань на конфігураційних проблемах;
  • мінуси: не бачить коду, погано покриває логіку за авторизацією, повільний.

SCA (Software Composition Analysis) - аналіз залежностей:

  • зіставляє версії пакетів з базами вразливостей, перевіряє ліцензії, покинуті пакети;
  • інструменти: composer audit, npm audit, Dependabot, Renovate, Snyk, OSV-Scanner, Trivy (ще й для Docker-образів).

Чого не знайде жоден автоматичний інструмент: логічні вразливості й контроль доступу - «чи може користувач A побачити замовлення B», «чи можна оплатити замовлення за ціною 0». Для цього - тести авторизації, рев'ю коду й ручне тестування.

Практичний набір для Laravel-проєкту:

  • у кожному PR: composer audit, PHPStan/Larastan, Semgrep чи Psalm з taint-аналізом для критичних частин;
  • за розкладом: DAST (ZAP baseline) проти staging;
  • Dependabot/Renovate для оновлень;
  • тести на авторизацію для кожного ендпойнта з ідентифікатором.

Головне - реакція на результати: інструмент, звіт якого ніхто не читає, лише створює ілюзію захищеності.

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

Більшість вразливостей потрапляє в код через звичайні PR. Рев'ю - найдешевше місце, щоб їх зупинити, якщо знати, куди дивитися.

1. Авторизація - перше питання до кожного нового ендпойнта:

  • чи перевіряється, що користувач має доступ саме до цього об'єкта (політика, authorize, пошук через власника)?
  • чи захищені нові методи Livewire-компонентів і дії Filament?
  • адміністративні функції - за правами, а не лише «прихована кнопка»?

2. Вхідні дані:

  • валідація з межами (довжина рядків, розмір масивів, діапазони чисел);
  • $request->validated() у create/update, а не $request->all();
  • ідентифікатори власника беруться з автентифікації, а не з тіла запиту.

3. Ін'єкції:

  • DB::raw, whereRaw, orderByRaw з даними користувача; назви колонок для сортування - з білого списку;
  • {!! !!} у Blade, v-html, dangerouslySetInnerHTML;
  • виконання команд (Process, exec), шляхи до файлів з введення користувача.

4. Дані на виході:

  • API Resource замість повернення моделі цілком;
  • чутливі поля не в журналах, відповідях і повідомленнях про помилки.

5. Секрети й конфігурація:

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

6. Зовнішні взаємодії:

  • HTTP-запити за URL з введення (SSRF);
  • вебхуки - перевірка підпису;
  • завантаження файлів - тип, розмір, зберігання поза public.

7. Залежності: нові пакети - чи підтримуються, чи потрібні взагалі, скільки тягнуть за собою.

8. Обробка помилок і стану:

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

Як зробити рев'ю ефективним:

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

Найчастіший пропуск - не ін'єкції, а відсутня перевірка прав у новому методі, що «просто повертає дані».

Докладніше в документації: OWASP Code Review Guide

Застарілі залежності - один з основних джерел вразливостей. Але бот, що щодня відкриває 30 пул-реквестів, швидко вчить команду їх ігнорувати. Мета - регулярні, невеликі, зрозумілі оновлення.

Два види оновлень:

  • security updates - негайні PR для версій з відомими вразливостями (Dependabot alerts);
  • version updates - планові оновлення до нових версій за розкладом.

Базова конфігурація Dependabot (.github/dependabot.yml):

version: 2
updates:
  - package-ecosystem: composer
    directory: /
    schedule:
      interval: weekly
    groups:
      laravel:
        patterns: ['laravel/*']
      minor-and-patch:
        update-types: [minor, patch]
    open-pull-requests-limit: 5

  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly

  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: monthly

Що робить процес керованим:

  • групування - усі патч- і мінорні оновлення одним PR на тиждень замість десятків окремих; пов'язані пакети (laravel/*, @vue/*) - разом;
  • розклад - щотижня в певний день, коли хтось точно подивиться;
  • ліміт відкритих PR;
  • автоматичне злиття патч-оновлень, якщо проходять тести (Renovate вміє це з коробки, для Dependabot - через правила репозиторію й workflow). Без хорошого покриття тестами автозлиття небезпечне;
  • мажорні оновлення - окремо й свідомо, з читанням журналу змін.

Renovate гнучкіший: пресети конфігурації, автозлиття за правилами, «dependency dashboard» з переліком усіх доступних оновлень, затримка для щойно опублікованих версій (minimumReleaseAge).

Затримка нових версій - корисний захист від атак на ланцюжок постачання: скомпрометовані версії пакетів зазвичай виявляють і відкликають протягом днів. Оновлення на версію, опубліковану кілька днів тому, а не годину, знижує ризик.

Не забувати:

  • lock-файли (composer.lock, package-lock.json) у репозиторії - щоб продакшен отримав рівно те, що протестовано;
  • оновлення GitHub Actions і Docker-образів - теж залежності;
  • CI має запускати всі тести на PR від бота - інакше оновлення «пройде», зламавши застосунок;
  • відповідальний за розбір - інакше PR накопичуються, і за пів року оновлення стає великим і страшним.

Докладніше в документації: GitHub: оновлення версій Dependabot

Багато зламів тривають тижнями й місяцями, перш ніж їх помітять, - часто ззовні (клієнти, журналісти, дослідники). Причина - журнали або не пишуться, або пишуться, але ніхто на них не реагує. OWASP Top 10:2025 називає категорію саме «журналювання й сповіщення»: записати замало, потрібна реакція.

Які події безпеки записувати:

  • автентифікація: вдалі й невдалі входи, вихід, скидання пароля, зміна email і пароля, увімкнення/вимкнення 2FA;
  • авторизація: відмови в доступі (403), особливо серійні - ознака перебору чужих id;
  • адміністративні дії: зміна ролей і прав, видалення записів, зміна налаштувань, вхід під іншим користувачем (impersonation);
  • операції з чутливими даними: експорт, масове вивантаження, перегляд персональних даних в адмінці;
  • помилки валідації й підозрілі запити у великих кількостях;
  • платіжні операції й зміни балансу;
  • зміни API-ключів і токенів.

Що має бути в записі: час (з поясом), хто (id користувача, а не лише IP), що зроблено і з чим, звідки (IP, User-Agent), результат, ідентифікатор запиту для зв'язку з іншими журналами.

Чого не має бути: паролі, токени, повні номери карток, зайві персональні дані. Журнали - теж джерело витоків.

Захист самих журналів:

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

Сповіщення - на події, що потребують реакції:

  • сплеск невдалих входів (перебір паролів, credential stuffing);
  • вхід адміністратора з нової країни чи пристрою;
  • масовий експорт даних;
  • серія 403/404 на ресурси з ідентифікаторами;
  • зміна прав адміністратора.

У Laravel: окремий канал журналу для подій безпеки, події Login, Failed, PasswordReset з пакета автентифікації, пакет журналу дій (наприклад, spatie/laravel-activitylog) для змін моделей, відправлення в централізовану систему (SIEM, ELK, Sentry, хмарні журнали) з правилами сповіщень.

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

Протокол SMTP не перевіряє відправника: будь-хто може надіслати лист з From: security@your-app.com. Три DNS-записи дозволяють поштовим сервісам відрізнити справжній лист від підробки.

SPF - які сервери мають право відправляти пошту домену:

your-app.com.  TXT  "v=spf1 include:mailgun.org include:_spf.google.com -all"

Отримувач перевіряє IP-адресу сервера-відправника. Обмеження: SPF перевіряє технічну адресу конверта (Return-Path), а не видимий користувачу From, і ламається при пересиланні листів.

DKIM - криптографічний підпис листа:

mg._domainkey.your-app.com.  TXT  "k=rsa; p=MIGfMA0..."

Поштовий сервіс підписує заголовки й тіло приватним ключем, отримувач перевіряє підпис публічним ключем з DNS. Лист не можна змінити дорогою, а підпис прив'язаний до домену.

DMARC - політика, що зв'язує все разом:

_dmarc.your-app.com.  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc@your-app.com; adkim=s; aspf=s"
  • вимагає вирівнювання (alignment): домен у видимому From має збігатися з доменом, що пройшов SPF чи DKIM. Саме це захищає від підробки адреси, яку бачить користувач;
  • політика: p=none (лише звіти), p=quarantine (у спам), p=reject (відхилити);
  • rua - щоденні агреговані звіти: хто й скільки листів відправляє від імені домену.

Чому це питання безпеки, а не лише доставки:

  • фішинг від вашого імені: без DMARC зловмисник надсилає «Скиньте пароль за посиланням» з вашого домену, і листи виглядають справжніми;
  • листи скидання пароля й коди входу, що потрапили в спам чи відхилені, - користувачі не можуть увійти й звертаються в підтримку, де їх легше обдурити соціальною інженерією;
  • з 2024 року Gmail і Yahoo вимагають від масових відправників (понад 5000 листів на день до Gmail) SPF, DKIM і DMARC, а від усіх відправників - щонайменше SPF чи DKIM; без них листи потрапляють у спам або відхиляються.

Як впроваджувати без зламаної пошти:

  1. SPF і DKIM для кожного сервісу, що відправляє від вашого домену (Mailgun, SES, CRM, служба підтримки);
  2. DMARC з p=none і звітами - кілька тижнів дивитися, хто ще відправляє;
  3. поступово quarantine, потім reject.

Окремий піддомен для транзакційної пошти (mg.your-app.com) ізолює її репутацію від маркетингових розсилок.

Домени без пошти теж треба захистити: v=spf1 -all і p=reject, інакше їх використають для підробки.

Докладніше в документації: RFC 7489: DMARC

Щоб зупинити застосунок, не потрібні мільйони запитів: досить кількох, кожен з яких змушує сервер робити величезну роботу. Захист від мережевих атак (CDN, WAF) тут не допомагає - запити виглядають цілком легітимно.

Типові «дорогі» запити:

Вектор Приклад
необмежена пагінація GET /api/orders?per_page=1000000
важкі фільтри й сортування сортування за колонкою без індексу, LIKE '%...%' по мільйонах рядків
великі тіла запитів JSON на сотні мегабайтів, масив з мільйоном елементів
файли гігабайтні завантаження, zip-бомба, «бомба декомпресії» в зображенні (крихітний PNG, що розпаковується в гігабайти пікселів)
регулярні вирази катастрофічний бектрекінг на рядку від користувача (ReDoS)
дорогі операції без ліміту генерація PDF, експорт, виклик LLM чи платного API на кожен запит
хешування дуже довгі паролі з дорогим алгоритмом хешування

Захист у Laravel:

// пагінація: верхня межа
$perPage = min($request->integer('per_page', 20), 100);

// валідація розмірів і кількостей
$request->validate([
    'items' => ['required', 'array', 'max:100'],
    'items.*.sku' => ['required', 'string', 'max:64'],
    'avatar' => ['image', 'max:2048', 'dimensions:max_width=4000,max_height=4000'],
    'password' => ['required', 'string', 'max:128'],
]);
  • сортування й фільтри лише зі списку дозволених колонок (які мають індекси);
  • обмеження частоти для дорогих маршрутів окремо: RateLimiter::for('exports', fn (Request $r) => Limit::perMinute(3)->by($r->user()->id));
  • важка робота - у черзі, з окремим пулом воркерів, щоб експорт не забирав процеси вебзапитів;
  • тайм-аути: statement_timeout / max_execution_time для бази, тайм-аути HTTP-клієнта до сторонніх сервісів.

Межі на рівні інфраструктури:

  • client_max_body_size у Nginx, post_max_size і upload_max_filesize у PHP - тіло відкидається ще до Laravel;
  • memory_limit і max_execution_time - щоб один запит не з'їв увесь сервер;
  • кількість воркерів PHP-FPM - обмежений ресурс: повільні запити займають їх, і решта користувачів чекає в черзі.

Неочевидні місця:

  • розпакування архівів - перевіряти сумарний розмір і кількість файлів під час розпакування, а не за розміром архіву;
  • зображення - перевіряти розміри в пікселях до обробки, а не лише розмір файлу;
  • GraphQL і вкладені include в API - обмеження глибини й складності запиту.

Перевірка: у тестах навмисно надсилати граничні значення (per_page=100000, масив на 10 000 елементів) і очікувати 422, а не 500 і не хвилину очікування.

Докладніше в документації: OWASP API4:2023 Unrestricted Resource Consumption

Завантаження файлів - один із найризикованіших механізмів: нападник контролює і вміст, і ім'я файлу.

Основні загрози:

  • Виконання коду: shell.php (або image.php.jpg при неправильно налаштованому сервері) у публічному каталозі - і нападник виконує довільний код.
  • Збережений XSS: SVG чи HTML-файл зі скриптом, відкритий з вашого домену.
  • Перезапис файлів через ім'я на кшталт ../../config/app.php.
  • Відмова в обслуговуванні: величезні файли, «zip-бомби», зображення з гігантською роздільною здатністю.

Захист:

  • Не довіряти імені й MIME-типу від клієнта. Генерувати власне ім'я (UUID) і визначати тип за вмістом. Розширення - з білого списку.
  • Зберігати поза веб-коренем або в об'єктному сховищі (S3), а віддавати через контролер чи підписаний URL.
  • Окремий домен для користувацьких файлів (usercontent.example) - тоді навіть шкідливий HTML не має доступу до cookie основного сайту.
  • Заборонити виконання в каталозі завантажень на рівні веб-сервера.
  • Обмежити розмір на всіх рівнях: веб-сервер, PHP (upload_max_filesize, post_max_size), валідація застосунку.
  • Перекодувати зображення (зменшити, пересохранити через GD/Imagick) - це прибирає вбудовані дані й метадані (EXIF з геолокацією).
  • Content-Disposition: attachment і X-Content-Type-Options: nosniff для файлів, які не мають відкриватися в браузері.
  • Антивірусна перевірка для файлів, які завантажуватимуть інші користувачі.
$request->validate([
    'avatar' => ['required', 'image', 'mimes:jpg,png,webp', 'max:2048', 'dimensions:max_width=4000,max_height=4000'],
]);

$path = $request->file('avatar')->store('avatars', 's3');   // ім'я генерує Laravel

SVG - окремий випадок: це XML зі скриптами. Його або забороняють, або санітизують, або віддають лише як attachment.

Докладніше в документації: OWASP: завантаження файлів

unserialize() у PHP відтворює не лише дані, а й об'єкти довільних класів з довільними значеннями властивостей. При цьому автоматично викликаються магічні методи: __wakeup(), __unserialize(), а згодом __destruct().

Якщо нападник контролює рядок для unserialize(), він може створити об'єкти класів, які вже є в застосунку чи його залежностях, з потрібними йому властивостями. Ланцюжок викликів магічних методів («POP chain») призводить до запису файлів, SQL-запитів чи виконання коду. Готові ланцюжки для популярних фреймворків і бібліотек відомі й зібрані в інструментах на кшталт PHPGGC.

// Вразливо
$cart = unserialize($_COOKIE['cart']);

Захист:

  • Не десеріалізувати дані від користувача. Для обміну даними - json_decode(): він створює лише масиви й stdClass, без виклику коду класів.
  • Якщо unserialize() неминучий - allowed_classes:
$data = unserialize($payload, ['allowed_classes' => false]);           // лише скаляри й масиви
$data = unserialize($payload, ['allowed_classes' => [Money::class]]);  // білий список
  • Підписувати серіалізовані дані, що проходять через клієнта (HMAC), і перевіряти підпис до десеріалізації.

Де це трапляється в Laravel-застосунках:

  • Laravel шифрує cookie й підписує завдання черги, тож напряму від користувача серіалізовані дані туди не потрапляють. Але витік APP_KEY дозволяє підробити зашифровані дані - і раніше це давало виконання коду через десеріалізацію. Тому витік ключа - критичний інцидент.
  • Кеш і сесії в Redis/Memcached серіалізуються: доступ нападника до кешу - теж шлях до цієї атаки. Laravel дозволяє обмежити класи, які можна десеріалізувати з кешу (serializable_classes у config/cache.php).
  • Phar-архіви: у старих версіях PHP файлові функції з шляхом phar:// десеріалізували метадані архіву. PHP 8.0 це прибрав.

Аналогічні проблеми мають pickle у Python, Java-серіалізація, YAML.load у Ruby.

Докладніше в документації: OWASP: десеріалізація

ReDoS (Regular expression Denial of Service) - регулярний вираз, який на спеціально підібраному рядку працює експоненційно довго. Один запит займає процесор на секунди чи хвилини; кілька - і сервер не відповідає.

Причина - катастрофічний бектрекінг. Більшість рушіїв регулярних виразів (PCRE у PHP, рушій JavaScript) при невдачі повертаються й пробують інші варіанти розбиття рядка. Вкладені квантифікатори дають експоненційну кількість варіантів:

^(a+)+$         на рядку "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"
^(\w+\s?)*$     на довгому рядку слів із символом у кінці, що не підходить
^(a|aa)+$

Небезпечні ознаки: вкладений квантифікатор ((x+)+, (x*)*), перекриваючі альтернативи під квантифікатором ((a|a)*, (\w|\d)+), і рядок, який майже підходить, але ламається в кінці.

Де шукати в застосунку:

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

Як поводиться PHP: PCRE має ліміти pcre.backtrack_limit і JIT-стек. При перевищенні preg_match повертає false (не 0!), а preg_last_error() - PREG_BACKTRACK_LIMIT_ERROR. Це рятує від нескінченного зависання, але:

  • до ліміту процесор усе одно працює;
  • код, що перевіряє if (! preg_match(...)), сприйме false як «не підходить» - і в деяких випадках це обхід валідації.

Захист:

  • переписувати вирази без неоднозначності: ^\w+(\s\w+)*$ замість ^(\w+\s?)*$, атомарні групи (?>...) і присвійні квантифікатори (a++), які забороняють повернення;
  • обмежувати довжину вводу перед регулярним виразом - найпростіший і дуже ефективний захист;
  • не приймати регулярні вирази від користувачів; якщо потрібно - рушій з лінійним часом (RE2) чи обмеження часу виконання;
  • перевіряти preg_last_error() і трактувати false як помилку;
  • готові валідатори (filter_var для email/URL, бібліотека libphonenumber) замість саморобних виразів;
  • статичні аналізатори (наприклад, правила Semgrep, safe-regex для JS) знаходять небезпечні шаблони.

Node.js особливо вразливий: однопотоковий цикл подій - один повільний регулярний вираз блокує обробку всіх запитів.

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

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

Рівні
Junior 35 Middle 37 Senior 31

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