Настройки 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.