Налаштування PHP за замовчуванням оптимізовані для розробки, а не для продакшн. На публічному сервері вони розкривають версію PHP і внутрішні шляхи, дозволяють небезпечні операції.
Знайти php.ini
php --ini | grep "Loaded Configuration"
# /etc/php/8.3/fpm/php.ini
У PHP-FPM і PHP CLI різні файли конфігурації навіть в одній системі — зазвичай /etc/php/8.3/fpm/php.ini і /etc/php/8.3/cli/php.ini. Правки в одному не застосовуються до іншого: налаштування для cron-завдань і консольних скриптів потрібно змінювати окремо.
Після правки будь-якого php.ini зміни не підхоплюються автоматично — PHP-FPM читає файл один раз при старті і тримає його в пам'яті воркера. Забутий перезапуск сервісу — часта причина, через яку «налаштування ніби застосоване», але перевірка показує старе значення.
sudo systemctl restart php8.3-fpm
Обов'язкові налаштування безпеки
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/errors.log
expose_php = Off
open_basedir = /var/www/site/:/tmp/:/var/lib/php/sessions/
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,phpinfo
file_uploads = On
upload_max_filesize = 10M
max_file_uploads = 5
max_execution_time = 30
memory_limit = 256M
Розбір помилок конфігурації
display_errors = On на продакшні
Якщо в цьому налаштуванні залишився On, будь-яка необроблена помилка PHP виводиться прямо у HTML-відповідь і видна кожному відвідувачу:
Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1045]
Access denied for user 'app'@'localhost' in /var/www/site/db.php:12
Stack trace:
#0 /var/www/site/index.php(5): connect()
#1 {main}
thrown in /var/www/site/db.php on line 12
Таке повідомлення розкриває повний шлях до файлів на сервері, ім'я користувача бази даних і структуру коду — цього достатньо, щоб спланувати наступний крок атаки. Помилки мають потрапляти в error_log, а не в браузер.
open_basedir налаштований лише для FPM
Якщо обмеження додано в /etc/php/8.3/fpm/php.ini, але не в cli/php.ini, консольні скрипти і cron-завдання виконуються без нього. Атака через веб-форму, яка передає дані у фонове завдання на CLI, обходить захист, розрахований лише на веб-запити.
У кожного віртуального хоста на спільному сервері може бути власний пул PHP-FPM зі своїм файлом налаштувань — якщо правите загальний php.ini, але конкретний сайт запущений через пул з окремим php_admin_value, що перевизначає потрібний параметр, зміна в загальному файлі не подіє саме на цей сайт.
Що змінює кожне налаштування
| Параметр | Значення за замовчуванням | Що дає хардненне налаштування |
|---|---|---|
| expose_php | On | Прибирає версію PHP із заголовка відповіді сервера |
| display_errors | On | Помилки йдуть у лог, а не у HTML-відповідь відвідувачу |
| open_basedir | не заданий | PHP не може читати файли поза вказаними директоріями |
| disable_functions | порожньо | Блокує запуск shell-команд із PHP-коду |
Безпека сесій
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Strict
session.use_only_cookies = 1
session.use_strict_mode = 1
session.gc_maxlifetime = 1440
session.save_path = /var/lib/php/sessions
Перевірка конфігурації
php -r "echo ini_get('expose_php');"
php -r "echo ini_get('display_errors');"
Перевірте також бойове середовище, а не лише CLI: запросіть заголовки реального сайту і переконайтеся, що версія PHP не розкривається веб-сервером.
curl -I https://ваш-домен/ | grep -i x-powered-by
# Порожній вивід — заголовок не віддається, все вірно
Коли жорсткі налаштування заважають роботі
- Застосунку для обробки зображень чи відео потрібен зовнішній бінарник через
shell_exec(наприклад, виклик окремого конвертера) — тоді зdisable_functionsприбирають лише конкретно потрібну функцію, а не залишають список порожнім повністю. - Спільний хостинг із кількома сайтами під різними користувачами на одному PHP-FPM пулі — єдиний
open_basedirдля всіх не підійде, кожному віртуальному хосту потрібен власний FPM-пул зі своїм значенням. - Легасі-код, який читає файли за межами каталогу сайту (наприклад, зі спільного каталогу з шаблонами) — такий код потрібно виправляти, розширення
open_basedir«про всяк випадок» повертає ту саму вразливість, від якої він захищає.
Налаштування веб-сервера перед PHP-FPM розбирає стаття хардненинг Nginx, а захист самого WordPress поверх цих налаштувань — хардненинг WordPress.