Crimson.Lab

Лаборатория Кримсона

21 October 2026

PHP 8.2.35 на CentOS 7: закрываем CVE-2026-109618 и CVE-2026-109619

21 октября 2026 года вышли PHP 8.5.12, 8.4.27, 8.3.36 и 8.2.35, закрывающие две критические уязвимости с возможностью удалённого выполнения кода без авторизации. А у нас — боевой Битрикс на CentOS 7, PHP 8.2.20 и замороженный репозиторий Remi для EL7. Обновлять сам Битрикс нельзя, откладывать исправление PHP тоже не хочется.

За вечер пересобрали PHP 8.2.35 из исходного пакета Remi, проверили обновление в контейнере с конфигами сервера и установили на прод. Перезапуск основного Apache занял 4,5 секунды, модули остались на месте, файловые сессии сохранились. Ниже — весь путь, включая грабли с OpenSSL и старой библиотекой PHP в памяти Apache.

Что обновляли

Компонент Наше окружение
ОС CentOS 7.9
Окружение BitrixVM 7.5
Веб-сервер nginx → Apache 2.4.6 prefork + mod_php
Панель BitrixVM Отдельный сервис httpd-scale
PHP 8.2.20 из remi-php82
OpenSSL Системный 1.0.2k
Битрикс 24.x, без возможности обновления

Битрикс уже работал на PHP 8.2. Для перехода с 8.2.20 на 8.2.35 менять его версию в нашем случае не понадобилось: это обновление внутри одной ветки с тем же API модулей — 20220829. Установленные PECL-расширения, включая APCu, memcache, geoip и zip, заработали без пересборки. Совместимость своего набора расширений и кода всё равно проверяем до установки.

Основная проблема — пакеты. Поддержка CentOS 7 закончилась 30 июня 2024 года, а репозиторий Remi для EL7 закрыт. Обычный yum update новый PHP в такое окружение не принесёт.

Что за уязвимости

Уязвимость Что происходит Подробности
CVE-2026-109618 Нехватка памяти прерывает инициализацию данных запроса. При очистке PHP может обратиться к некорректному состоянию. GHSA-w8hw-w9fx-j79w
CVE-2026-109619 В рабочем процессе остаются ссылки на уже освобождённую память предыдущего запроса. Следующий запрос может обратиться к этому состоянию. GHSA-6wmf-4rv5-p3rp

Обе получили 9,2 по CVSS 4.0. Команда PHP предупреждает о риске для установок, обслуживающих веб-запросы: mod_php, FPM, CGI, FrankenPHP и LiteSpeed. Для Apache с mod_php подтверждена эксплуатация до запуска кода приложения — авторизация в Битриксе здесь не помогает.

Исправления вошли в 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12. Для неподдерживаемых PHP 7.4, 8.0 и 8.1 команда PHP предоставила минимальные бэкпорты в исходниках; это не возобновление выпуска релизов этих веток. Ссылки на них есть в официальном сообщении.

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

Где взять комплект

Скрипты, патч и готовые пакеты лежат в assets/blog/php-update. Можно забрать только этот каталог:

git clone --depth 1 --filter=blob:none --sparse \
  https://github.com/william-aqn/blog.git blog-php-update
git -C blog-php-update sparse-checkout set assets/blog/php-update
cd blog-php-update/assets/blog/php-update

Команды ниже рассчитаны на Bash и машину с Docker для Linux-контейнеров x86_64. Сборку выполняем из каталога комплекта. Адрес сервера подставляем свой; эти переменные понадобятся для копирования файлов и запуска команд по SSH:

SERVER=root@server.example.com
REMOTE_DIR=/root/php-8.2.35

Сначала временные меры

Пока собирается новый PHP, server/mitigate.sh позволяет изменить настройки нашего окружения:

В нашем Битриксе использование session.upload_progress не обнаружилось: проверили ядро и local/, а совпадение в модуле clouds оказалось идентификатором DOM-элемента. Для своих доработок это лучше проверить отдельно.

Передаём скрипты и запускаем временные меры на сервере:

ssh "$SERVER" "mkdir -p '$REMOTE_DIR'"
scp -r server "$SERVER:$REMOTE_DIR/"
ssh "$SERVER" "cd '$REMOTE_DIR' && LIMIT_MB=64 bash server/mitigate.sh"

64M/60M — наши рабочие ограничения, выбранные с учётом загрузок пользователей. В рекомендациях PHP предложены более низкие значения post_max_size: 8M без загрузки файлов и 32M при её необходимости. Скрипт меняет основной конфиг nginx; переопределения в виртуальных хостах и фактически применённые настройки веб-PHP нужно проверить отдельно.

Для чтения нового php.ini mod_php достаточно graceful-перезагрузки. Но рекомендация PHP требует обновить все рабочие процессы: если старые воркеры продолжают обслуживать запросы, один успешный reload ещё не означает, что настройка действует везде.

В рекомендациях против CVE-2026-109618 есть memory_limit=-1. Мы сознательно его не включали: на сервере MySQL занимал больше 150 ГБ, swap отсутствовал. Неограниченное потребление памяти PHP создало бы риск системного OOM и остановки других сервисов, включая MySQL. Это был выбор для нашего сервера на время подготовки обновления, а не равноценная замена исправлению.

Собираем RPM под CentOS 7

Dockerfile создаёт окружение на CentOS 7 с архивными репозиториями CentOS, EPEL 7 и Remi EL7. build.sh скачивает php-8.2.35-1.remi.src.rpm, проверяет архив исходников PHP, адаптирует spec и собирает RPM.

docker build -t php82-el7-build build/
docker run --rm -v "$PWD/out:/out" php82-el7-build

На машине с 8 vCPU установка зависимостей заняла около четырёх минут, сама сборка — около шести. Результат:

Путь Содержимое
out/RPMS/x86_64/ Бинарные RPM и SHA256SUMS
out/SRPMS/php-8.2.35-1.el7.local.src.rpm Пересобранный исходный пакет
out/build.log Лог сборки

Собираются варианты NTS и ZTS, как в прежних пакетах Remi для EL7. Регрессионные тесты PHP в этом сценарии отключены флагом --without tests; после сборки отдельно проверяем обновление пакетов и запуск mod_php.

Если Docker подключён к удалённому демону, bind mount использует файловую систему того хоста, на котором работает демон. Локальный $PWD туда не передаётся. Для такой сборки можно запустить контейнер без --rm и без bind mount, а результат забрать через docker cp. Для дальнейшего smoke-теста конфиги и тестовый скрипт также должны быть доступны контейнеру.

Архивные репозитории и новый формат SRPM

Обычные зеркала CentOS 7 больше не подходят. Базовые пакеты берём с vault.centos.org/7.9.2009, EPEL 7 — из dl.fedoraproject.org/pub/archive/epel/7. Настройки находятся в vault.repo, а Dockerfile проверяет заданные отпечатки ключей EPEL и Remi.

У исходного пакета Remi в заголовке есть rpmlib(RichDependencies), поэтому старый RPM из CentOS 7 не может просто установить его через rpm -i. Распаковываем пакет через rpm2cpio, после чего rpmbuild разбирает spec уже в окружении EL7.

Подпись этого SRPM старый rpm 4.11 тоже не смог проверить. В сборочном скрипте есть сверка php-8.2.35.tar.xz с SHA-256, опубликованной на php.net. Она подтверждает содержимое архива PHP, но не заменяет проверку подписи всего SRPM: spec и дополнительные патчи в эту сверку не входят.

ICU и OpenSSL

Для php-intl spec требует ICU не ниже 74, а в используемом Remi EL7 доступен libicu73. Снижаем требование до 73. Остальные изменения spec добавляют и применяют патч OpenSSL; полный diff лежит рядом.

С OpenSSL сборка упала в ext/openssl/xp_ssl.c:

subscripted value is neither array nor pointer nor vector

Новый код вызывает ASN1_STRING_get0_data(), которой нет в системном OpenSSL 1.0.2. В соседнем openssl.c предусмотрена совместимость со старым API, а этому файлу понадобилась такая же замена. Внутрь существующего условия PHP_OPENSSL_API_VERSION < 0x10100 добавили:

#define ASN1_STRING_get0_data(_asn1) ((const unsigned char *) ASN1_STRING_data(_asn1))

Полный патч для OpenSSL 1.0.2 включён в комплект. После сборки проверили отсутствие implicit declaration of function в логе и наличие используемых символов OpenSSL в системных libssl.so.10 и libcrypto.so.10.

Проверяем обновление в контейнере

test/smoke-test.sh устанавливает старый PHP 8.2.20, выбранные PECL-пакеты и Apache, затем подставляет конфиги с сервера. После обновления до наших RPM сравнивает список модулей, содержимое каталога php.d и выводит ответ тестовой страницы через mod_php. Заодно скачивает старые пакеты для отката в out/rollback/.

Сохраняем конфиги сервера на машине сборки:

ssh "$SERVER" \
  'tar -C / -czf - etc/php.ini etc/php.d etc/httpd/conf.d/php.conf etc/httpd/conf.modules.d/20-php.conf' \
  > prod-php-conf.tgz

Список пакетов берём со своего сервера. Вывод с именами исходных RPM помогает отделить подпакеты PHP от остальных:

ssh "$SERVER" "rpm -qa 'php*' --qf '%{NAME} %{SOURCERPM}\n'"

В CORE_PACKAGES помещаем пакеты из исходного RPM php-8.2.*, в PECL_PACKAGES — остальные расширения и PHP-библиотеки. Наш набор выглядел так:

CORE_PACKAGES=(
  php php-bcmath php-cli php-common php-gd php-ldap php-mbstring
  php-mysqlnd php-opcache php-pdo php-process php-pspell php-sodium php-xml
)
PECL_PACKAGES=(
  php-pecl-apcu php-pecl-geoip php-pecl-mcrypt php-pecl-memcache
  php-pecl-rrd php-pecl-runkit7 php-pecl-xdebug3 php-pecl-zip php-pear php-zipstream
)

docker run --rm \
  -v "$PWD/out:/out" \
  -v "$PWD/test:/test:ro" \
  -v "$PWD/prod-php-conf.tgz:/prod-php-conf.tgz:ro" \
  -e CORE="${CORE_PACKAGES[*]}" \
  -e PECL="${PECL_PACKAGES[*]}" \
  --entrypoint bash php82-el7-build /test/smoke-test.sh

У нас список модулей до и после совпал, mod_php ответил 8.2.35 apache2handler, новых ошибок PHP в error_log не появилось.

Этот тест воспроизводит окружение PHP, но не разворачивает копию Битрикса с базой данных. Работу самого сайта, авторизацию, загрузки файлов, обмены и фоновые задания проверяем отдельно на тестовой копии. Ответ страницы и журнал Apache тоже нужно просмотреть: скрипт выводит их, но не превращает любую ошибку HTTP или PHP в неуспешный результат теста.

Устанавливаем на сервер

Перед установкой нужны резервная копия сервера и комплект старых RPM. Каталог out/rollback/ готовит предыдущий шаг; снимки списков пакетов и конфигов внутри установщика полноценный бэкап не заменяют.

Есть ещё одна особенность server/install.sh: перед рестартом он принудительно завершает дочерние процессы старше STUCK_AGE, по умолчанию 3600 секунд. Проверяется возраст процесса, а не длительность запроса. Долго живущий воркер может быть исправен, поэтому перед запуском нужно оценить этот порог для своего сервера; принудительное завершение оборвёт его текущую работу.

Копируем новые RPM вместе с SHA256SUMS, старые пакеты и скрипты:

ssh "$SERVER" "mkdir -p '$REMOTE_DIR'"
scp -r out/RPMS/x86_64 out/rollback server "$SERVER:$REMOTE_DIR/"
ssh "$SERVER" "cd '$REMOTE_DIR' && bash server/install.sh x86_64"

Установщик выполняет следующие действия:

  1. Находит установленные подпакеты, собранные из исходного RPM PHP, и подбирает для них новые файлы. PECL и PEAR остаются прежними.
  2. Проверяет выбранные файлы по SHA256SUMS и настройку PrivateTmp у httpd.service.
  3. Сохраняет списки модулей и пакетов, содержимое php.d и контрольные суммы конфигов в /root/php-update-*.
  4. Выполняет rpm -Uvh --test, затем устанавливает пакеты через rpm -Uvh.
  5. Сравнивает список модулей. Если он изменился, останавливается до рестарта Apache — нужно разобраться или откатить пакеты.
  6. Завершает процессы по описанному выше порогу и полностью перезапускает httpd, затем активные httpd-scale и php-fpm.
  7. Ищет старый PHP-код в памяти процессов, выводит результат HTTP-запроса и последние строки журнала Apache.

Для конфигов используется %config(noreplace): изменённые локально файлы сохраняются, а новые варианты при необходимости появляются как .rpmnew.

Проверка PrivateTmp нужна из-за файловых сессий. Если они находятся в приватном /tmp сервиса, остановка может удалить их вместе с этим каталогом. В нашем BitrixVM PrivateTmp был выключен через custom.conf, а сессии лежали в /tmp/php_sessions. Установщик останавливается при PrivateTmp=yes; обходить эту проверку без выяснения session.save_path не стоит.

По умолчанию проверочный URL — http://127.0.0.1:8888/, как в нашем BitrixVM. Для другого порта или виртуального хоста задаём CHECK_URL. Успешный HTTP-ответ проверяем вместе с версией PHP через веб-SAPI: один php -v показывает только CLI. Код ответа и журналы установщик выводит для просмотра, а не использует как полноценную проверку работоспособности сайта.

Почему reload недостаточно

После замены RPM файл libphp.so на диске уже новый, но Apache может продолжать выполнять старый код из памяти. В нашем окружении systemctl reload httpd перечитывал php.ini, однако не заменял загруженную библиотеку. Для обновления mod_php потребовался полный рестарт всех использующих его экземпляров Apache.

Самая неприятная находка — httpd-scale. Панель BitrixVM с сентября 2024 года продолжала работать на библиотеке PHP 8.1, хотя пакеты на диске давно обновили до 8.2. В памяти оставался libphp.so;66d725d6 (deleted), а при reload в журнал попадало:

Module compiled with module API=20220829, PHP compiled with module API=20210902

После рестарта проверяем, не осталось ли процессов с удалённой старой библиотекой:

grep -lE 'libphp\.so(;| \(deleted\))' /proc/[0-9]*/maps

При достаточных правах и отсутствии ошибок чтения совпадений быть не должно. Эта проверка дополняет проверку версии через веб, а не заменяет её.

С длительностью рестарта тоже был нюанс. В нашем systemd-юните остановка Apache выполнялась через SIGWINCH, то есть graceful-stop с ожиданием завершения запросов. Два воркера находились в poll() и жили уже 81 и 89 дней. Мы завершили их перед рестартом, чтобы не ждать TimeoutStopSec в 90 секунд. Именно для этого случая в установщике появился порог возраста процессов.

Что получилось на проде

Проверка Результат нашего обновления
Пакеты Обновлены 14 подпакетов PHP
Модули и конфиги Список модулей, файлы php.d и настройки сохранились
Основной Apache Перезапуск за 4,5 секунды
Панель httpd-scale Перезапуск за 1,2 секунды
Старый код в памяти Не обнаружен ни у одного из 238 процессов httpd
Ошибки после обновления В просмотренных логах не было 502 и PHP Fatal
Файловые сессии Около 73 тысяч сессий сохранились, пользователей не разлогинило

В просмотренных отрезках журнала nginx доля ответов 5xx составляла 2,03% до обновления и 1,74% после. Это наблюдение по конкретному серверу, а не доказательство влияния новой версии PHP на ошибки сайта.

Как откатить

Если после обновления обнаружилась несовместимость, используем server/rollback.sh и сохранённые старые RPM:

ssh "$SERVER" "bash '$REMOTE_DIR/server/rollback.sh' '$REMOTE_DIR/rollback'"

Скрипт выбирает установленные пакеты, выполняет rpm -Uvh --oldpackage и полностью перезапускает Apache и активные дополнительные сервисы. Конфиги он из бэкапа не восстанавливает. Откат к 8.2.20 возвращает и уязвимую версию PHP, поэтому это способ восстановить работоспособность на время разбора проблемы.

Готовые пакеты и дальнейшие обновления

В комплекте есть 32 готовых RPM для x86_64 без debuginfo и их SHA256SUMS. Это пакеты из той же сборки, которую мы установили на сервер: 14 использованных там пакетов совпали по контрольным суммам.

Также доступны исходный RPM для EL7 и его SHA256SUMS. Готовый SRPM можно пересобрать через rpmbuild --rebuild в подготовленном окружении CentOS 7. Для проверки готовых бинарных пакетов тем же smoke-тестом сначала копируем содержимое rpms/x86_64/ в ожидаемый им каталог out/RPMS/x86_64/.

Версия в build.sh задаётся переменной PHP_VERSION, но для следующего релиза нужно заново проверить spec, применимость патча OpenSSL и результаты тестов. Одной замены номера недостаточно, чтобы считать сборку проверенной.

CentOS 7 после этого обновления не становится поддерживаемой системой. Мы закрыли конкретные уязвимости PHP и выиграли время для переноса сайта на поддерживаемую ОС. Сам перенос, версии библиотек и совместимость Битрикса проверяются отдельно. У ветки PHP 8.2 исправления безопасности запланированы до 31 декабря 2026 года, так что откладывать следующий этап надолго уже не получится.

Продолжение: PHP 7.4 на CentOS 7 после окончания поддержки — бэкпорт для старой ветки, проверка расширений и обновление второго сервера.

tags: php - bitrix - centos - security - docker