21 октября 2026 года PHP выпустил 8.5.12, 8.4.27, 8.3.36 и 8.2.35 с исправлением двух критических уязвимостей, CVE-2026-109618 и CVE-2026-109619 (CVSS v4.0 9.2). Обе позволяют выполнить код на сервере без авторизации, и уязвим не какой-то плагин, а сам интерпретатор: любой сайт на PHP, от WordPress до самописного кода.
На обычном сервере история короткая: обновить пакет и перезапустить PHP. На хостинге с ispmanager и CloudLinux всё оказалось интереснее. За ночь и утро мы закрыли уязвимости на всех серверах и по пути наступили почти на все грабли, которые не описаны ни в бюллетене PHP, ни в документации панели.
Это не пересказ бюллетеня, а список того, что пошло не по инструкции, и как мы это решили. Если вы администрируете сервер с ispmanager, особенно на CloudLinux, проверьте у себя каждый пункт: часть проблем не видна, пока не посмотришь в нужное место.
Временные меры до выхода патчей
Пока пакетов нет, полностью закрыть уязвимости нельзя, можно только отрезать известные пути атаки. Публичные эксплоиты, появившиеся ещё до релиза, использовали обработчик прогресса загрузки файлов, который срабатывает на любой запрос multipart/form-data, даже если на сайте нет ни одной формы загрузки.
Мы сделали две вещи:
- Отключили
session.upload_progress.enabledво всех версиях PHP. Это рекомендация самого PHP, функцию почти никто не использует, сайты от её отключения не страдают. Важно: значение должно задаваться вphp.iniили в конфигурации пула, а не черезini_set()или.user.ini, иначе оно применится слишком поздно. - Заблокировали
multipartна уровне nginx для всех сайтов. В ispmanager каждый vhost подключает/etc/nginx/vhosts-includes/*.conf, поэтому одного файла хватает на весь сервер:
if ($http_content_type ~* "multipart") {
return 403;
}
Цена второй меры: перестают работать загрузки файлов через формы, вложения в веб-почте, импорт в phpMyAdmin. Поэтому это мера на часы, а не на дни.
Два места, которые легко пропустить:
- Дефолтный
serverв/etc/nginx/nginx.conf. Он ловит запросы по IP и с неизвестнымHostи в ispmanager проксирует их в Apache, ноvhosts-includesне подключает. Без отдельногоincludeтуда атака по IP проходит мимо блока. - Apache на внешнем адресе. Если Apache слушает не
127.0.0.1, а внешний IP, запрос можно отправить прямо ему, минуя nginx. Проверка:ss -ltnp | grep httpd.
Ловушка CageFS: настройка из панели не доходит до сайтов
Днём мы отключили session.upload_progress.enabled штатно: «Настройки web-сервера → PHP → версия → настройки». Панель показала, что значение сохранено. Вечером проверка изнутри клетки CageFS показала, что у сайтов оно по-прежнему включено.
Причина в том, как устроен alt-php на CloudLinux:
- ispmanager записал значение в
/opt/alt/phpXX/etc/php.d.all/session.ini, то есть в ini расширения session; - расширение session в alt-php встроено и через PHP Selector не подключается, поэтому этот ini внутри клетки никто не читает;
- внутри клетки PHP читает основной
/opt/alt/phpXX/etc/php.iniи пользовательскийalt_php.iniиз/opt/alt/phpXX/link/conf, а это совсем другой каталог, чем видит root.
Отсюда главный урок: проверять PHP нужно от имени пользователя внутри клетки, а не от root:
su -s /bin/bash -c "/opt/alt/php83/usr/bin/php -r 'var_dump(ini_get(\"session.upload_progress.enabled\"));'" ЛОГИН_КЛИЕНТА
Что сработало:
- Дописать директиву в конец
php.iniкаждой версии, который действительно загружается (путь даётphp --ini, строкаLoaded Configuration File). - Выполнить
cagefsctl --rebuild-alt-php-ini, чтобы пересобрать пользовательскиеalt_php.ini. - Выполнить
cagefsctl --update: у нативного PHP внутри клетки своя копия/etc/php.iniв скелете CageFS, и без обновления скелета правка до сайтов не доходит. - Не забыть персональные
php.iniсайтов в режиме CGI: ispmanager держит их в/var/www/ПОЛЬЗОВАТЕЛЬ/data/php-bin*/php.ini. - Перезапустить httpd и FPM.
И ещё одна деталь: в ту же ночь в 03:37 что-то пересобрало php.ini всех alt-php из шаблона. Если правка живёт только в php.ini, её стоит подстраховать задачей cron, которая проверяет и восстанавливает строку, пока патчи не вышли. Мы так и сделали, и cron несколько раз действительно её возвращал, например после обновления пакетов alt-php.
Блок multipart ломает вход в панель
Через пару часов после блокировки клиент написал, что не может войти в панель: запрос входа получает 403, а интерфейс бесконечно показывает индикатор загрузки. IP клиента никто не блокировал.
Панель ispmanager работает на своём веб-сервере ihttpd на порту 1500, и при входе напрямую на этот порт nginx действительно ни при чём. Но если включён модуль nginx_proxy, панель доступна и через 443 по адресу https://ХОСТ/ispmgr, а форма входа отправляется как multipart/form-data. Наш блок отвечал 403 ещё в nginx, а фронтенд панели такую ошибку показать не умеет.
Модуль подключается только в vhost хоста панели (/etc/nginx/modules-includes/nginx_proxy/), и все его пути уходят в ihttpd, а не в PHP. Поэтому исключение безопасно делать по связке хоста и пути:
set $cve_mp 0;
if ($http_content_type ~* "multipart") { set $cve_mp 1; }
set $cve_hu "$host:$uri";
if ($cve_hu ~* "^panel\.example\.com:/(ispmgr|mancgi|manimg|long|shell|notify)(/|$)") { set $cve_mp 0; }
if ($cve_mp = 1) { return 403; }
Исключение только по пути без хоста было бы ошибкой: запрос на клиентский-сайт/ispmgr ушёл бы в Apache, а там WordPress перепишет его на index.php, и PHP разберёт multipart. На хосте панели этот путь гарантированно уходит в ihttpd.
Список путей лучше брать из самого модуля (grep location /etc/nginx/modules-includes/nginx_proxy/*.conf), а не по памяти. И проверять сразу после перезагрузки nginx не стоит: первые запросы может ещё обслужить старый воркер со старой конфигурацией.
Забытые Roundcube и phpMyAdmin
Когда вышли патчи alt-php, мы обновили пакеты и сняли блок. И только потом вспомнили, что веб-почта и phpMyAdmin в ispmanager работают не на той версии PHP, которую вы выбрали для сайтов. Обновление alt-php их не касается.
Оба приложения подключаются через vhosts-includes, то есть доступны на каждом домене сервера (/roundcube, /webmail, /phpmyadmin или путь со случайным префиксом). Какой PHP их выполняет, видно по fastcgi_pass в этих файлах:
grep -E 'location|fastcgi_pass' /etc/nginx/vhosts-includes/*roundcube*.conf /etc/nginx/vhosts-includes/*phpmyadmin*.conf
У нас нашлось сразу несколько вариантов на разных серверах:
| Приложение | Через nginx | Через Apache |
|---|---|---|
Roundcube /roundcube | нативный PHP-FPM (php-fpm.apache.sock) или isp-php74 | isp-php74 через свой сокет |
Roundcube /webmail | нативный PHP-FPM | mod_php |
| phpMyAdmin | нативный PHP-FPM | mod_php |
Вторая колонка важна не меньше первой. Apache получает эти запросы в двух случаях:
- запрос по IP или с неизвестным
Hostуходит через дефолтный server nginx прямо в Apache, гдеAlias /webmailи/phpmyadminглобальные; - в конфигурации nginx для
/webmailи/phpmyadminстоятerror_page 404 @apacheиerror_page 502 = @apache. Адрес с PATH_INFO вроде/webmail/index.php/xне совпадает сlocation ~ \.php$, получает 404 и уходит в Apache, где его выполнит mod_php.
На одном из серверов нативный PHP был 5.4 из CentOS 7, для которого исправлений не будет никогда. Пока мы переводили эти приложения на alt-php, блок multipart оставался только на их путях, а сайты уже работали без ограничений:
if ($uri !~* "^/(roundcube|webmail|phpmyadmin)(/|$)") { set $cve_mp 0; }
Клиенты в это время не могли прикреплять вложения в веб-почте, и они это заметили почти сразу.
Почему на CloudLinux номер версии не меняется
CloudLinux выпустил исправления для всех веток alt-php сразу, от 5.4 до 8.5, но не в виде 8.3.36 или 8.4.27, а как новые сборки прежних версий: alt-php83-8.3.35-6, alt-php84-8.4.26-5, alt-php85-8.5.11-5 и так далее. Патч вносится в установленную версию, растёт только номер релиза пакета.
Поэтому скрипты проверки, которые сравнивают PHP_VERSION с 8.3.36, будут показывать «уязвимо» и после обновления. А клиенты, заглянувшие в панель, увидят те же цифры и спросят, почему ничего не обновлено. Надёжных признаков два.
Changelog пакета. Для старых веток CloudLinux дополнительно вырезал обработчик прогресса загрузки целиком:
for p in $(rpm -qa --qf '%{NAME}\n' 'alt-php[0-9]*' | grep -E '^alt-php(5[4-9]|[78][0-9])$' | sort -V); do
printf '%-10s %-16s ' $p "$(rpm -q --qf '%{VERSION}-%{RELEASE}' $p)"
rpm -q --changelog $p | head -10 | grep -c 'CVE-2026-10961[89]'
done
Build Date в phpinfo(). У исправленных сборок там 21 октября 2026, у предыдущих более ранние даты. Это то, что можно сообщить клиентам: проверить может любой, без доступа к серверу.
С нативным PHP та же история с другой стороны. На RHEL 9 и его пересборках исправленная версия 8.0 называется php-8.0.30-8.el9_8.1 (бюллетень Red Hat RHSA-2026:87151), а в changelog номера CVE оказались приклеены к последнему пункту списка с совсем другим описанием. Без бюллетеня по changelog это легко принять за обычное обновление.
У пакетов ispmanager (isp-phpXX) changelog вообще шаблонный, с одной записью 2019 года. Признаки исправления там косвенные: номер сборки (у нас 1781 для 8.2-8.5, 1782 для 7.4, 1786 для 7.1) и Build Date 22 октября.
Подводные камни самого обновления
dnf --best --allowerasing на сервере с ispmanager. На CloudLinux 9 dnf update вываливал длинный список конфликтов: в репозитории есть нативный PHP 8.3.36 из модульного потока, и dnf считает его лучшим кандидатом для установленного 8.0. Но модуль Apache от ispmanager (isp-httpd-mod_php) жёстко требует PHP 8.0.x, а от него зависят coremanager-pkg-php и ispmanager-pkg-php, то есть сама панель. Подсказка dnf добавить --best --allowerasing здесь опасна: она удалит пакеты панели. Конфликт безвреден, в конце dnf пишет Nothing to do. Чтобы убрать шум до выхода сборки 8.0: dnf update --exclude='php,php-*'.
Модуль Apache для PHP у ispmanager свой. Он грузится из modules/isp-libphp8.so и принадлежит пакету isp-httpd-mod_php, а не системному php. Исправления этих CVE находятся в ядре PHP, то есть внутри самого модуля, и обновление пакета php его не меняет. Проверка:
grep -rhE 'LoadModule\s+php' /etc/httpd/conf.modules.d/ /etc/httpd/conf.d/
rpm -qf /etc/httpd/modules/isp-libphp8.so
strings /etc/httpd/modules/isp-libphp8.so | grep -m1 -E '[A-Z][a-z]{2} [ 0-9][0-9] 20[0-9]{2}'
Если модуль собран раньше 21 октября, все сайты в режиме «Модуль Apache» остаются уязвимыми. Найти их: grep -lE 'SetHandler\s+application/x-httpd-php\s*$' /etc/httpd/conf/vhosts/*/*. Мы перевели такие сайты на alt-php, а там, где это невозможно (интерфейс, которому нужен доступ вне CageFS), закрыли доступ по IP.
Выключенный PHP хуже включённого. Часть бывших mod_php-сайтов оказалась статикой, и мы выключили им PHP. На одном из них рядом с заглушкой лежал 61 файл старого сайта, и при выключенном PHP их исходники, включая конфиг с паролем от базы, отдавались как текст. Перед выключением PHP проверяйте: find ДОКРУТ -name '*.php*' | wc -l.
Обрыв SSH посреди транзакции. Обновление 160 пакетов alt-php шло несколько минут, и связь оборвалась. Транзакция, к счастью, завершилась, но проверять нужно: dnf history list, dnf check (на CentOS 7: package-cleanup --problems, package-cleanup --dupes). Длинные обновления лучше запускать в tmux.
Тихие пересборки ispmanager. Исправленные isp-php выходили в течение суток по одному: сначала 8.2-8.5, потом 7.4, потом 7.1. Для CentOS 7 сборка 7.4 на момент написания так и не появилась. Имеет смысл несколько раз в день проверять dnf check-update 'isp-php*'.
Как перевести Roundcube и phpMyAdmin на alt-php
На сервере с нативным PHP 5.4 мы перевели служебный пул FPM apache на alt-php56, сохранив тот же сокет. Тогда конфигурацию nginx менять не нужно, а откат сводится к запуску старого сервиса. Выбор 5.6 вместо 7.4 простой: Roundcube 1.4 и старый phpMyAdmin работали на 5.4, 5.6 ближе всего, и CloudLinux выпустил для неё бэкпорт.
Шаги:
- Свой каталог ini. Вне CageFS alt-php от root подключает только минимум расширений, остальные лежат в
php.d.all. Мы собрали одинextensions.iniиз нужных модулей (session, json, dom, xml*, mbstring, intl, gd, mcrypt, zip, pdo, nd_mysql*). Ini-файлы CloudLinux сами подтягивают зависимости, поэтому дубликаты строкextension=убираем. - Конфиг FPM с пулом
apacheна/var/run/php-fpm.apache.sock, теми жеuser,group,listen.ownerи явнымsession.save_path, потому что в нативномphp.iniон не был задан. - Проверка до переключения:
PHP_INI_SCAN_DIR=… php -mбез ошибок иphp-fpm -t -y …сtest is successful. - systemd-юнит с
Environment=PHP_INI_SCAN_DIR=…иExecStart=… php-fpm --nodaemonize -y …. - Переключение: переименовать
/etc/php-fpm.d/apache.conf, перезапустить нативный FPM (у него остаётся пулwww), запустить новый. Простой несколько секунд. - Apache тоже на этот сокет. В
roundcube-webmail.confиphpmyadmin-httpd.confзаменитьSetHandler application/x-httpd-phpнаSetHandler "proxy:unix:/var/run/php-fpm.apache.sock|fcgi://localhost/", вroundcube-isp.confзаменить сокет isp-php74. Проверить, чтоSetHandlerстоит внутри<FilesMatch "\.php$">, иначе в FPM уйдёт и статика.
Удаление дубликатов расширений из шага 1:
awk '/^[[:space:]]*(zend_)?extension[[:space:]]*=/ { k=$0; gsub(/[[:space:]]/,"",k); if (seen[k]++) next } { print }' $FILES > conf.d/extensions.ini
Проверка, кто реально выполняет PHP на каждом пути, включая прямой запрос в Apache и PATH_INFO:
echo '<?php echo PHP_VERSION, " ", php_sapi_name();' > /usr/share/roundcubemail/_v.php
curl -s -H 'Host: nonexistent.invalid' http://127.0.0.1:8080/webmail/_v.php
curl -sk https://ДОМЕН/webmail/_v.php/x
rm -f /usr/share/roundcubemail/_v.php
Везде должно быть 5.6.40 fpm-fcgi. После этого блок nginx можно снимать.
Два минуса решения: на пути через Apache пропадают ограничения open_basedir из конфигов для mod_php, а roundcube-isp.conf помечен ispmanager как перезаписываемый при обновлении пакета. После обновлений панели стоит проверять, что сокеты не вернулись и что /etc/php-fpm.d/apache.conf не появился заново.
Чек-лист для сервера с ispmanager
Если у вас сервер с ispmanager и вы ещё не уверены, что закрыли PHP2Shell полностью, пройдитесь по списку:
- Все используемые ветки alt-php обновлены, в changelog есть CVE-2026-109618/109619 или
Build Dateне раньше 21 октября 2026. - Пакеты
isp-php*обновлены, сервисыphp-fpm*перезапущены (после смены unit-файла сначалаsystemctl daemon-reload). - Нативный PHP ОС обновлён или ничего не обслуживает (проверьте пулы в
/etc/php-fpm.d/и кто подключается к их сокетам). - Модуль Apache
isp-libphp8.soсобран после 21 октября, или ни один сайт не работает в режиме «Модуль Apache». - Roundcube и phpMyAdmin работают на исправленном PHP и через nginx, и через Apache.
- Значения PHP проверены от имени пользователя внутри CageFS, а не от root.
- Скелет CageFS обновлён (
cagefsctl --update). - Временные блоки в nginx сняты, включая строку в дефолтном
serverвnginx.conf. - На сайтах с выключенным PHP нет
.php-файлов. - Клиентам объяснено, почему номер версии в панели не изменился.
Главный вывод этой ночи простой: на хостинге с панелью «обновить PHP» означает не один пакет, а пять-шесть разных сборок PHP от трёх поставщиков (апстрим через CloudLinux, ispmanager, дистрибутив), и часть из них обслуживает не сайты, а служебные приложения панели. Полезно заранее знать, какой PHP за каким путём стоит, до того как он понадобится ночью.