Несмотря на зрелость PHP и большую экосистему, многие разработчики упускают из виду редкие уязвимости, скрывающиеся в стандартных и малоизученных механизмах языка. Изучив их, вы сможете укрепить безопасность приложений и предотвратить неожиданные атаки.

 1. Введение

PHP – один из самых популярных языков веб-разработки. Благодаря экосистеме фреймворков (Laravel, Symfony, Yii) и обширному опен-сорсу он остаётся выбором №1 для быстрого вывода приложений. При этом сообщество в основном концентрируется на классических уязвимостях (SQL-инъекции, XSS, CSRF, LFI/RFI). Тонкие особенности интерпретатора, малоизученных расширений и встроенных механизмов нередко остаются вне поля зрения. В этой статье мы подробно рассмотрим малоизвестные опасности и способы защиты от них.

2. Уязвимость сериализации объектов: wakeup() и destruct()

Магические методы wakeup() и destruct() вызываются при десериализации и уничтожении объекта соответственно. В сочетании с неконтролируемыми данными это позволяет запускать побочные эффекты и вредоносный код.

 Расшире��ный сценарий

  1. Модификация конфигураций – через свойства объекта подставляются пути к конфигам, и в __wakeup() выполняется include/require.

  2. Потенциальный побочный эффект — при десериализации злоумышленник может вызвать деструктор объекта, внутри которого выполняется curl_exec(), отправляющий данные на внешний сервер.

  3. Отложенный запуск – с помощью register_shutdown_function() внутри сериализуемых объектов можно отложить выполнение до завершения скрипта, что осложняет отладку.

Пример

<?php
class Notifier {
    private $url;
    private $payload;

    public function __construct(string $url, array $payload) {
        $this->url = $url;
        $this->payload = $payload;
    }

    public function __wakeup() {
        // Отправка запроса на внешний API (опасно при неконтролируемых данных)
        $ch = curl_init($this->url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $this->payload);
        curl_exec($ch);
        curl_close($ch);
    }

    public function __destruct() {
        // Очистка временного файла (может удалить любой путь)
        if (!empty($this->payload['file'])) {
            @unlink($this->payload['file']);
        }
    }
}

Если злоумышленник контролирует сериализованную строку, он может подставить URL атакующего и путь к конфиденциальному файлу.

Защита

  • Никогда не вызывать unserialize() на данных от пользователя.

  • При необходимости использовать опцию allowed_classes для белого списка допустимых классов.

  • Переходить на безопасные форматы сериализации: JSON (с валидацией по схеме), YAML с валидацией.

  • Разделять десериализацию и критичные операции: валидировать свойства перед выполнением побочных эффектов.

  • Минимизировать использование магических методов в классах, которые могут принимать внешние данные.

3. «Промежуточные» атаки через stream-фильтры

Stream-фильтры позволяют обрабатывать данные «на лету». При некорректной проверке параметров можно получить доступ к исходному коду, конфигам и даже базе данных.

Примеры атак

  • php://filter/convert.iconv.UTF-8/CP1251/resource=config.php – чтение файла в другой кодировке и дальнейшая инъекция.

  • php://filter/zlib.inflate/resource=backup.sql.gz – распаковка дампа БД «на лету».

Защита

  1. Запрет любых php:// и data:// префиксов при приёме имён файлов: проверять регуляркой, например preg_match('/^[a-z0-9_\-]+\.php$/i', $file).

  2. Всегда применять realpath() и сверять директорию: strpos(realpath($file), BASE_PATH) === 0.

  3. Отключить в php.ini параметры, если не требуются:

    o         allow_url_include = Off

    o         allow_url_fopen = Off (если не нужен доступ к URL-ресурсам)

  4. На уровне приложения: валидировать и нормализовать пути, использовать белые списки.

4. Побочные эффекты слабой типизации при сравнении ключей массивов

PHP автоматически преобразует строковые числовые ключи в целые, что приводит к коллизиям при создании массивов.

Демонстрация

$data = [
    "00"   => "zero",
    0      => "int_zero",
    0.0    => "float_zero",
    false  => "bool_zero",
    null   => "null_zero"
];

var_export($data); // ['0' => 'null_zero']

Все записи сольются, и в массиве останется лишь последний элемент.

Угрозы

  • Неправильная агрегация ролей при хранении флагов.

  • Фильтрация/поиск по ключу может давать ложные срабатывания.

Рекомендации

  • Приводить ключи к строковому виду: $key = (string)$key;

  • Использовать объекты или структуры SPL:

    o        SplFixedArray для индексированных коллекций;

    o        SplObjectStorage для хранения объектов-ключей.

  • Явно проверять типы (===) при сравнениях.

5. Тайм-ауты и неожиданное поведение c URL-мультизапросов

curl_multi_exec() может блокировать всю группу запросов из-за одного «зависшего» дескриптора.

Особенности

  • При возврате curl_multi_select() значения -1 дескриптор может быть испорчен.

  • Без обработки CURLM_CALL_MULTI_PERFORM цикл может зациклиться.

Пример безопасного шаблона

$mh = curl_multi_init();
foreach ($urls as $url) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT_MS, 3000);
    curl_multi_add_handle($mh, $ch);
}

do {
    while (($mrc = curl_multi_exec($mh, $active)) === CURLM_CALL_MULTI_PERFORM);
    if ($mrc !== CURLM_OK) break;

    while ($info = curl_multi_info_read($mh)) {
        $ch = $info['handle'];
        if ($info['result'] !== CURLE_OK) {
            // логирование ошибки
        }
        curl_multi_remove_handle($mh, $ch);
        curl_close($ch);
    }

    if ($active) {
        $select = curl_multi_select($mh, 1.0);
        if ($select === -1) {
            // исправление «ошибочных» сокетов
            usleep(100);
        }
    }
} while ($active);

curl_multi_close($mh); 

6. Потенциальные дыры в intl-расширении

Расширение intl опирается на ICU; уязвимости в ICU могут приводить к краху процесса или более серьёзным проблемам.

Кейсы

  • Переполнение буфера при форматировании сложных скриптов (арабский, иврит).

  • Ошибки в разборе даты в нестандартных локалях (например, fa_IR).

Как быть

  • Мониторить CVE для ICU и обновлять ICU-библиотеку и пакеты ОС.

  • Ограничивать длину и набор символов у пользовательских шаблонов через preg_match.

  • Обрабатывать IntlException и не допускать «fallback» на дефолтные локали без уведомления.

7. «Невидимые» XSS через невидимые юникод-символы

Невидимые символы (ZWJ, ZWNJ, NBSP, RLM, LRM и т. п.) позволяют «расщеплять» ключевые слова HTML-атрибутов и обходить фильтрацию.

Пример

Атака с вставкой невидимого символа:

<img src=x on\u200Dmouseover=alert(1)>

Браузеры объединяют символы и могут выполнить onmouseover.

Нормализация и защита

  1. Использовать Normalizer::normalize($str, Normalizer::FORM_C).

  2. Удалять все управляющие/неотображаемые символы: preg_replace('/\p{Cf}/u','',$str).

  3. Применять whitelist-фильтрацию и специализированные библиотеки – например, HTML Purifier с кастомным набором тегов/атрибутов.

8. Утечки данных через pcntl в многопроцессном режиме

Процессы, созданные через pcntl_fork(), наследуют память родителя (copy-on-write). Без явного удаления секретов и запуска сборщика мусора конфиденциальная информация остаётся доступной в адресном пространстве дочерних процессов.

Последствия

  • Утечка API-ключей, паролей, токенов в дочерних процессах;

  • Накопление «мусора» и рост памяти при некорректном завершении дочерних процессов.

Безопасный шаблон использования fork

<?php
$sensitive = 'SECRET';
$pid = pcntl_fork();
if ($pid === -1) {
    // ошибка fork
    exit(1);
} elseif ($pid === 0) {
    // дочерний процесс
    unset($sensitive);          // удалить секреты из видимой области
    gc_enable();
    gc_collect_cycles();        // попытаться освободить память
    // выполнить задачу...
    exit(0);
} else {
    // родитель
    pcntl_wait($status);
}

Рекомендации

  • Удалять (unset) все переменные с конфиденциальной информацией перед fork, если это возможно.

  • Использовать минимальный набор глобальных переменных в родительском процессе.

  • Ограничивать продолжительность жизни дочерних процессов и корректно их завершать/перезапускать.

  • Рассмотреть альтернативы (системные демоны, очереди, RoadRunner, Swoole).

9. Особенности OPcache, ведущие к гонкам и потенциальному RCE при динамическом создании кода

OPCache кэширует байткод файлов. При динамическом создании/перезаписи скриптов возможна гонка между записью на диск и инвалидацией кэша – приложение может выполнить старую (или неконсистентную) версию файла.

Пример проблемы

  1. file_put_contents('/tmp/job.php', $code);

  2. include '/tmp/job.php';

Если OPcache ещё содержит старую версию или не успел корректно её инвалировать – может выполниться некорректный/неожиданный байткод.

Меры предосторожности

  • После записи файла вызывать opcache_invalidate('/tmp/job.php', true);

  • Настроить opcache.revalidate_path, opcache.validate_timestamps и opcache.file_update_protection в соответствии с моделью деплоя.

  • Избегать записи исполняемых PHP-скриптов в общедоступные временные директории – лучше хранить код в версиях/деплое и использовать безопасные механизмы очередей или планировщиков задач.

  • При динамическом исполнении кода обрабатывать содержимое как данные, а не как исполняемый файл (парсить/проверять / использовать безопасные контейнеры).

10. Риск расширений с C-модулями

Расширения на C (Imagick, ZipArchive, др.) работают на уровне нативного кода. Ошибки в нём приводят не только к «крашу» скрипта, но и к падению интерпретатора или потенциальному выполнению произвольного кода.

Критичные кейсы

  • Обработка вредных/повреждённых бинарных форматов изображений (ImageMagick / imagick) – возможный RCE/DoS.

  • Чтение повреждённых ZIP – переполнение/крах в ZipArchive.

  • Уязвимости в нативных драйверах для БД.

Профилактика

  • Ограничивать входной набор форматов и максимальный размер файлов на уровне веб-сервера (NGINX/Apache) и приложения.

  • Запускать обработку бинарных данных в изолированных контейнерах (chroot, контейнеры, отдельные сервисы) или с пониженными привилегиями.

  • Использовать AppArmor/SELinux для ограничения прав.

  • Автоматически отслеживать CVE и обновлять расширения/библиотеки.

11. Особенности PHP-плагинов в CMS (WordPress / Joomla / Drupal)

Плагины часто пишут непрофессионалы, в результате появляются скрытые бэкдоры, небезопасный eval, открытые REST-эндпоинты и неправильно настроенные хуки.

Типичные угрозы

  • eval(base64_decode(...))  в коде плагина (часто встречается в админской панели);

  • REST-эндпоинты, принимающие команды shell;

  • Переопределение хуков и интеграция злоумышленного кода через add_action/hook_node_view.

Практические меры

  • Жёсткие права на каталог uploads (запрет выполнения файлов), минимизация прав на файлы.

  • Ежедневный/регулярный скан плагинов: WPScan, специализированные сканеры для Joomla/Drupal.

  • Настройка CSP (Content-Security-Policy), ограничение внешних скриптов и источников.

  • Ревью кода сторонних плагинов перед установкой; использовать только проверенные плагины и держать их в актуальном состоянии.

12. Особенности файловой системы и автозагрузки (Composer, PSR-4)

Composer-автолоадер (PSR-4, classmap, ключ files) даёт гибкость, но при неверных настройках возможна загрузка нежелательных файлов или затенение классов.

Проблемы

  • Дублирование классов и «затенение» безопасных реализаций.

  • Использование files в composer.json может подключать произвольный код во время загрузки.

Защита

  • Минимизировать использование files, отдать предпочтение PSR-4.

  • Для production-среды включить classmap-authoritative и optimize-autoloader.

  • Проводить composer validate и ревью composer.json при PR/деплое.

13. Уязвимости в фоновых очередях (Gearman, RabbitMQ и др.)

Payload задач в очередях часто не валидируются строго: в них могут попадать исполняемые фрагменты, шелл-команды или сериализованные объекты.

Реальные атаки

  • Передача аргументов, которые используются в exec()/system().

  • Внедрение PHP-кода в тело задачи, который затем выполняется через eval()/include.

Защита

  • Валидировать и хранить payload в безопасном формате (JSON с JSON-schema).

  • Разделять права: выделенный системный пользователь для демонов очередей с минимальными правами доступа.

  • Логирование и мониторинг аномалий в payload.

14. Побочные эффекты при использовании eval() и assert()

eval() и (в старых версиях) assert() при включённых опциях выполняют строковый PHP-код – это прямой путь к RCE при недостаточной фильтрации.

Примеры риска

  • assert($_GET['code']); – исполнение пользовательского кода при старом синтаксисе.

  • eval("return {$userCode};"); – обход ограничений через вложенные кавычки и спецсимволы.

Рекомендации

  • Полностью отказаться от eval()/опасных assert() и заменить их безопасными альтернативами.

  • Для анализа/выполнения выражений использовать парсер (например, nikic/php-parser) и whitelist-подход к AST.

  • Если нужен «песочникообразный» парсинг – выносить выполнение в изолированный процесс/контейнер.

15. Заключение и краткий чеклист (ключевые меры)

  • Избегать небезопасной десериализации: использовать JSON/YAML + валидацию, allowed_classes при unserialize().

  • Нормализовать и фильтровать входные данные: stream-фильтры, юникод-символы, HTML-атрибуты.

  • Устанавливать явные таймауты для сетевых запросов и форков, контролировать дескрипторы c URL.

  • Регулярно обновлять PHP-расширения, ICU, Composer-зависимости, отслеживать новые CVE.

  • Ограничивать обработку бинарных данных в C-модулях и запускать их в изоляции (контейнеры, AppArmor/SELinux).

  • Аудитировать плагины CMS, применять жёсткие права доступа на файловой системе.

  • Избегать eval()/assert(), минимизировать динамическую автозагрузку файлов.

  • Во всех фоновых механизмах (Gearman, RabbitMQ и т. п.) – валидировать payload через схемы и изолировать процессы.

Внимание к деталям и понимание внутренних механизмов PHP и его расширений помогут значительно повысить безопасность приложений и защититься от редких, но опасных атак.

Комментарии (5)


  1. shuchkin
    06.11.2025 15:38

    ха, да, еще не оставляйте config.php.bak на проде


    1. Fragster
      06.11.2025 15:38

      ну всякую конфигурацию можно вынести на уровень выше папки веб публикации и забыть (в определенной мере, конечно) про этот класс уязвимостей (таких как доступность /.git и прочих служебных файлов с чувствительной информацией, что бывает намного чаще).