Питання на співбесіді з Безпека
Питання з реальних співбесід з відповідями: 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).
Сервер, щойно отримавши публічну 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 і Адамом Шостаком):
- Над чим ми працюємо? - модель системи;
- Що може піти не так? - загрози;
- Що ми з цим робимо? - заходи;
- Чи добре ми впоралися? - перевірка.
Модель системи - діаграма потоків даних (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 хвилин обговорення для функцій, що зачіпають автентифікацію, платежі, файли, зовнішні інтеграції, персональні дані; результат - перелік загроз і задач у трекері, а не документ на полиці.
Три класи автоматизованих перевірок безпеки шукають різне і доповнюють одне одного.
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» перевіряється тестом, а не на око.
Найчастіший пропуск - не ін'єкції, а відсутня перевірка прав у новому методі, що «просто повертає дані».
Застарілі залежності - один з основних джерел вразливостей. Але бот, що щодня відкриває 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, хмарні журнали) з правилами сповіщень.
Протокол 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; без них листи потрапляють у спам або відхиляються.
Як впроваджувати без зламаної пошти:
- SPF і DKIM для кожного сервісу, що відправляє від вашого домену (Mailgun, SES, CRM, служба підтримки);
- DMARC з
p=noneі звітами - кілька тижнів дивитися, хто ще відправляє; - поступово
quarantine, потімreject.
Окремий піддомен для транзакційної пошти (mg.your-app.com) ізолює її репутацію від маркетингових розсилок.
Домени без пошти теж треба захистити: v=spf1 -all і p=reject, інакше їх використають для підробки.
Щоб зупинити застосунок, не потрібні мільйони запитів: досить кількох, кожен з яких змушує сервер робити величезну роботу. Захист від мережевих атак (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.
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.
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 особливо вразливий: однопотоковий цикл подій - один повільний регулярний вираз блокує обробку всіх запитів.
Питання з реальних технічних співбесід - 103 питання у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії