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.enabledчерез/etc/php.d/z_bx_custom.ini. - Уменьшить
post_max_sizeдо 64M,upload_max_filesizeдо 60M и существующийclient_max_body_sizeв основном конфиге nginx до 64m. - Сохранить копии изменяемых файлов в
/root/php-cve-backup-*, проверить конфигурацию nginx и Apache, выполнить reload.
В нашем Битриксе использование 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"
Установщик выполняет следующие действия:
- Находит установленные подпакеты, собранные из исходного RPM PHP, и подбирает для них новые файлы. PECL и PEAR остаются прежними.
- Проверяет выбранные файлы по
SHA256SUMSи настройкуPrivateTmpуhttpd.service. - Сохраняет списки модулей и пакетов, содержимое
php.dи контрольные суммы конфигов в/root/php-update-*. - Выполняет
rpm -Uvh --test, затем устанавливает пакеты черезrpm -Uvh. - Сравнивает список модулей. Если он изменился, останавливается до рестарта Apache — нужно разобраться или откатить пакеты.
- Завершает процессы по описанному выше порогу и полностью перезапускает
httpd, затем активныеhttpd-scaleиphp-fpm. - Ищет старый 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