Что относится к конфигурации безопасности 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] -. не отдаются напрямую .-> B
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] -. не отдаются напрямую .-> B
Безопасная конфигурация PHP начинается с границ: наружу открыт только public-вход, а приватные файлы, uploads, сессии и логи не лежат в публичной зоне.

Dev и 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 = On

error_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 и ограниченный доступ, а не через публичный вывод.

Быстрое повторение
Почему на production обычно ставят `error_reporting = E_ALL`, но `display_errors = Off`?

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 — плохая идея: он показывает конфигурацию, расширения, пути, переменные окружения и иногда слишком много контекста.

Быстрое повторение
Какую роль выполняет `expose_php = Off` и почему это не полноценная защита?

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 это легко пропустить.

Быстрое повторение
В проекте есть `/app/public/index.php`, `/app/vendor`, `/app/storage` и `.env`. Куда должен смотреть document root и почему?

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 = 5

post_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 = Off

allow_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 = 1440

session.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 и окружении, где реально крутится приложение.

См. также

Источники

  1. PHP Manual: Runtime Configuration — Error Handling
  2. PHP Manual: Description of core php.ini directives
  3. PHP Manual: Runtime Configuration — Sessions
  4. PHP Manual: Securing Session INI Settings
  5. PHP Manual: Runtime Configuration — Filesystem
  6. PHP Supported Versions
  7. OWASP Cheat Sheet Series: PHP Configuration Cheat Sheet