Что относится к конфигурации безопасности PHP
Конфигурация безопасности PHP — это не один «секретный» флаг, а набор ограничений вокруг рантайма: что показывать пользователю, какие файлы принимать, как хранить session id, можно ли подключать удалённые ресурсы, где лежит document root и какая версия PHP вообще обслуживает запрос. Это слой ниже приложения, но он напрямую влияет на последствия ошибок в коде.
Важно не путать уровни. php.ini не заменяет Prepared statements и SQL injection, XSS, экранирование вывода и шаблоны или CSRF и state-changing запросы. Он уменьшает blast radius: случайный warning не раскрывает путь к файлу, upload-директория не превращается в место исполнения PHP, session cookie труднее украсть, а старый PHP не остаётся без security fixes.
flowchart TD
A[Браузер] --> B[Веб-сервер]
B --> C[Document root: /public]
C --> D[index.php]
D --> E[PHP-FPM / выбранный SAPI]
E --> F[Код приложения]
F --> G[(База данных)]
F --> H[(Сессии вне document root)]
F --> I[(Uploads вне document root или без исполнения PHP)]
F --> J[(Логи ошибок)]
K[.env, src, vendor, storage] -. не отдаются напрямую .-> BDev и production — разные профили
На локальной машине удобно видеть ошибки сразу. На production это утечка: stack trace, абсолютные пути, SQL-фрагменты, имена классов, параметры запроса. Поэтому практичный минимум выглядит так:
; php.ini для production
expose_php = Off
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/app-error.log
; PHP 7.4+
zend.exception_ignore_args = Onerror_reporting = E_ALL не означает «показывать всё пользователю». Это означает «считать все ошибки значимыми». Дальше вы выбираете канал: в dev — на экран, в production — в лог. display_errors = Off скрывает ошибки из HTTP-ответа, log_errors = On сохраняет их для расследования. Если приложение отдаёт JSON API, HTML warning посреди JSON особенно неприятен: клиент получает сломанный ответ и иногда ещё внутренние детали сервера.
Для разработки можно держать отдельный профиль:
; php.ini для разработки
error_reporting = E_ALL
display_errors = On
display_startup_errors = On
log_errors = OnНо не надо «временно» включать display_errors на боевом домене. Временные debug-флаги чаще всего забывают выключить. Если нужна диагностика, включайте её через логи, observability и ограниченный доступ, а не через публичный вывод.
expose_php, версии и SAPI
expose_php = Off убирает лишний сигнал вроде X-Powered-By: PHP/.... Это не защита от взлома само по себе: attacker всё равно может догадаться о PHP по URL, cookie, ошибкам или поведению приложения. Но раскрывать точную технологию и версию без необходимости не стоит.
Отдельная проверка — версия PHP. Если ветка вышла из support, конфигурационные флаги не компенсируют отсутствие security fixes. Это связано с Версии PHP и режимы выполнения: CLI, FPM, Apache module и встроенный сервер могут читать разные php.ini. Проверяйте именно тот SAPI, который обслуживает запросы.
php --ini
php -i | grep "Loaded Configuration File"Для PHP-FPM дополнительно смотрят pool-конфиг и страницу диагностики только в приватной среде. Публичный phpinfo() на production — плохая идея: он показывает конфигурацию, расширения, пути, переменные окружения и иногда слишком много контекста.
Document root: наружу только public/
Один из самых важных production-принципов: document root должен смотреть не в корень проекта, а в публичную директорию. Например, у приложения структура может быть такой:
/app
/public
index.php
assets/app.css
/src
/vendor
/storage
.env
composer.jsonВеб-сервер должен отдавать только /app/public. Тогда .env, composer.json, исходники, логи, кэш и приватные uploads не становятся статическими файлами. Это не PHP-директива, а настройка Nginx/Apache/Caddy, но для PHP-безопасности она базовая.
Плохой smell: URL вида /vendor/autoload.php или /storage/logs/app.log физически достижим из браузера. Даже если сервер «не исполняет» эти файлы, он может их отдать как текст. Для фреймворков это обычно уже решено стандартным public/, но в самописном PHP это легко пропустить.
Upload limits и upload-директории
Загрузки файлов — отдельная зона риска, подробно она связана с Загрузка файлов. На уровне php.ini вы ограничиваете объём и поверхность:
file_uploads = On
upload_tmp_dir = /var/lib/myapp/php-upload-tmp
upload_max_filesize = 10M
post_max_size = 12M
max_file_uploads = 5post_max_size должен быть больше upload_max_filesize, потому что multipart-запрос содержит не только сам файл, но и поля формы с техническими границами. memory_limit обычно должен быть больше post_max_size, хотя правильный обработчик больших файлов не должен целиком читать их в память.
Если приложение не принимает файлы вообще, file_uploads = Off — нормальное решение. Если принимает, временная директория должна быть writable для PHP-процесса, но не обязана быть доступна из веба. Постоянное хранилище uploads лучше держать вне document root или настроить так, чтобы PHP там не исполнялся. Файл avatar.php.jpg не должен получить шанс стать скриптом из-за ошибки веб-сервера.
allow_url_include и удалённые ресурсы
allow_url_include = Off почти всегда должен оставаться выключенным. Этот флаг разрешает include, require, include_once, require_once работать с URL-aware wrappers. В сочетании с ошибкой вроде include $_GET['page']; это резко приближает приложение к Remote File Inclusion.
allow_url_include = Offallow_url_fopen шире: он влияет на функции, которые читают URL «как файл», например file_get_contents('https://...'). Для старого легаси-кода его иногда оставляют включённым, но для нового кода лучше использовать явный HTTP-клиент, таймауты, allowlist доменов и проверку ответа. В любом случае не передавайте пользовательский URL напрямую в файловые функции. Это уже область SSRF и валидации ввода, рядом с GET, POST и фильтрация ввода.
Session settings
Базовая настройка сессий должна соответствовать тому, что уже обсуждалось в Куки и сессии и CSRF и state-changing запросы: session id ходит в cookie, не в URL, cookie защищена флагами, а CSRF-токен не равен session id.
session.use_cookies = 1
session.use_only_cookies = 1
session.use_strict_mode = 1
session.use_trans_sid = 0
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax
session.cookie_lifetime = 0
session.gc_maxlifetime = 1440session.use_strict_mode = 1 заставляет PHP отвергать session id, который не был создан session-модулем. session.use_trans_sid = 0 не даёт протаскивать session id через URL. HttpOnly снижает риск кражи cookie через XSS, Secure требует HTTPS, SameSite=Lax обычно хорошо подходит для обычных сайтов; Strict жёстче, но может ломать некоторые переходы и внешние auth-flow. Для embedded, OAuth или cross-site сценариев решение нужно принимать осознанно, а не копировать флаг вслепую.
В коде эти параметры часто задают до session_start():
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();Это удобно, когда разные окружения собираются из одного образа, а параметры приходят из env. Главное — не вызвать session_start() раньше.
Короткий production-чеклист
Минимальный набор для ревью: PHP-ветка поддерживается; production SAPI действительно читает нужный php.ini; display_errors и display_startup_errors выключены; ошибки логируются; expose_php выключен; document root указывает на public/; .env, vendor, storage, логи и исходники недоступны по HTTP; upload limits соответствуют продукту; uploads не исполняют PHP; allow_url_include выключен; session cookie имеет Secure, HttpOnly, подходящий SameSite; session id не передаётся через URL.
И ещё одна практичная проверка: после деплоя смотрите не только конфиг-файл в репозитории, а фактическое поведение. Сделайте запрос к боевому домену, проверьте заголовки, попробуйте открыть заведомо приватный путь, убедитесь, что ошибка превращается в нормальный 500-ответ без stack trace. Конфигурация безопасности ценна только в том SAPI и окружении, где реально крутится приложение.
См. также
- Версии PHP и режимы выполнения — почему версия, SAPI и
php.iniдолжны проверяться вместе. - SAPI и суперглобалы — где PHP получает request metadata и почему CLI/FPM могут отличаться.
- Загрузка файлов — безопасная обработка
$_FILES, MIME, расширений и хранения. - Куки и сессии — session cookie, lifetime и флаги безопасности.
- CSRF и state-changing запросы —
SameSiteкак дополнительный слой, а не замена токенам. - XSS, экранирование вывода и шаблоны — почему
HttpOnlyне отменяет экранирование вывода. - Ошибки, исключения и Throwable — как ошибки становятся исключениями и логируются в приложении.
Источники
- PHP Manual: Runtime Configuration — Error Handling
- PHP Manual: Description of core php.ini directives
- PHP Manual: Runtime Configuration — Sessions
- PHP Manual: Securing Session INI Settings
- PHP Manual: Runtime Configuration — Filesystem
- PHP Supported Versions
- OWASP Cheat Sheet Series: PHP Configuration Cheat Sheet