21 октября команда PHP выпустила внеплановые версии 8.5.12, 8.4.27, 8.3.36 и 8.2.35. В них закрыты две уязвимости с оценкой 9.2 из 10, которые в отрасли уже прозвали PHP2Shell: обе позволяют выполнить чужой код на сервере, и для этого не нужен ни вход на сайт, ни уязвимый плагин. Задеты все способы запуска PHP — FPM за nginx, модуль Apache, CGI. Если на вашем сервере работает PHP, этот текст про вас.
Ниже — что именно сломано (своими словами, без рецептов атаки), как за пять минут проверить свои серверы, почему «просто обновить» иногда не получается и что смотреть в журналах, пока обновление готовится. Это не пересказ новости: я прошёл всё это на серверах, с которыми работаю, накануне и сегодня.
Коротко
| Что | Ответ |
|---|---|
| исправленные версии | 8.5.12, 8.4.27, 8.3.36, 8.2.35 — и всё, что выйдет после |
| номера | CVE-2026-109618 и CVE-2026-109619, обе CVSS 4.0 — 9.2 |
| что нужно атакующему | только отправить запросы на сайт; авторизация не нужна |
| кого задевает | все ветки до исправленных версий, все SAPI; тяжелее всего — модуль Apache (mod_php) |
| 8.1 и старше | официальных выпусков не будет: срок поддержки кончился |
| атаки | несколько компаний сообщают о попытках эксплуатации, массовые — с 21 октября; детали ещё уточняются |
| временная настройка, которая закрывает дыру | нет. Позиция PHP: единственный надёжный путь — обновление |
Что сломано — на пальцах
Обе ошибки живут не в вашем коде и не в WordPress, а в самом интерпретаторе, на этапе, когда PHP готовит очередной запрос к выполнению. Поэтому никакой плагин безопасности их не видит: он запускается позже.
- CVE-2026-109618 — порча памяти при её нехватке. Если на определённом шаге подготовки запроса PHP упирается в
memory_limit, он прерывается на полпути и оставляет внутренние структуры в несогласованном виде. Из такого состояния, при определённом стечении условий, можно добраться до выполнения своего кода; - CVE-2026-109619 — «остатки» предыдущего запроса. PHP-FPM не запускает новый процесс на каждый запрос: один рабочий процесс обслуживает сотни запросов подряд. Между ними состояние должно полностью сбрасываться. Ошибка в том, что часть глобального состояния переживает запрос и достаётся следующему — в том числе запросу другого посетителя.
Вторая ошибка хорошо объясняет, почему уязвимы все SAPI: повторное использование процесса — это суть и FPM, и модуля Apache. А модуль Apache пострадал сильнее потому, что, по опубликованным разборам, там срабатывание возможно ещё до того, как управление дойдёт до кода приложения.
Частая ошибка в эти дни — «у нас всё за WAF / за Cloudflare, значит, не страшно». Фильтр на входе видит обычные HTTP-запросы и может отсеять известные сигнатуры, но признаком атаки здесь служит не странный адрес, а поведение интерпретатора. Фильтр снижает шум, а не риск.
Проверить свои серверы: пять минут
Главное правило: смотреть версию того PHP, который реально обслуживает сайт, а не того, что первым нашёлся в PATH. На хосте может стоять один PHP, в контейнере — другой, а у панели хостинга — третий.
# все контейнеры с PHP на Docker-хосте: имя, версия, образ
for c in $(docker ps --format '{{.Names}}'); do
v=$(docker exec "$c" sh -c 'php -r "echo PHP_VERSION;"' 2>/dev/null) || continue
echo "$c $v $(docker inspect -f '{{.Config.Image}}' "$c")"
done
Как правило, вывод получается пёстрым. Вот типичная картина сервера, на котором проекты копились несколько лет:
| Что работает | Версия | Вывод |
|---|---|---|
| основные сайты на WordPress, общий образ | 8.3.24 | отстаёт на 12 выпусков, хотя «обновляли регулярно» |
| сайт на старой копии того же образа | 8.3.12 | отстаёт на 24 выпуска |
| старый проект на чужом базовом образе | 8.2.3 | ветка жива, но образ не наш — пересобрать нечем |
| служебный сервис, Apache с модулем PHP, открыт в интернет | 8.1.7 | худший случай: mod_php и ветка без поддержки |
| веб-панель базы данных за паролем прокси | 7.4.33 | без патча навсегда; спасает только то, что до PHP не дойти без пароля |
После обновления обязательно перезапустите PHP — FPM, Apache или контейнер. Рабочие процессы держат в памяти старый код интерпретатора: пакет обновился, файлы на диске новые, а сайт обслуживает старая версия. Проверяйте версию у работающего процесса, а не у пакета.
Почему «просто обновить» не всегда получается
Самое неприятное открытие этих двух дней не про саму уязвимость. Возьмём образ PHP, собранный на Fedora 40 с пакетами из популярного стороннего репозитория, — именно так было в одном из образов, с которыми я работаю. Попытка обновиться показала: для этой версии дистрибутива репозиторий давно перестал выпускать пакеты и остановился на 8.3.24. Всё это время пересборка образа проходила без ошибок — она просто ставила ту же самую 8.3.24. Ни предупреждения, ни сбоя: образ молча замёрз.
А в день выпуска тот же репозиторий объявил, что обновлений сегодня не будет вовсе, и посоветовал пакеты дистрибутива или сборку из исходников. Итог: даже на поддерживаемой системе «обновить пакет» оказалось невозможно, нужна смена базы образа.
- Проверяйте, что обновление действительно что-то обновляет. Пересборка без ошибок — не доказательство. После сборки выводите
php -vи сравнивайте с последним выпуском на php.net; - Официальные образы Docker Hub (
php:8.3.36-fpm,php:8.3.36-apacheи т. д.) получают исправления в день выпуска. Если ваш образ на другой базе, на время аварии это самый короткий путь; - Плавающий тег — не решение.
FROM php:8-apacheпри пересборке притянет уже 8.5, и старый проект на 8.1 может не завестись. Закрепляйте точную версию:php:8.2.35-apache; - Кэш сборки может держать старую базу. Если CI собирает базовый слой один раз и переиспользует его, пока не изменился Dockerfile, то
FROM php:8.5-fpmтак и останется 8.5.11, сколько бы раз вы ни перезапускали сборку. Поменяйте версию в Dockerfile явно — это и обновит образ, и сбросит кэш.
Пока обновление готовится
Переход на новую базу образа — это не пять минут: другие пути к конфигам, другой пользователь, другой набор расширений, и всё это надо проверить на тестовой копии. На это время есть несколько мер, которые не закрывают уязвимость, но сужают круг тех, кто до неё дотянется:
- Уберите из интернета то, чему там не место. Служебные панели, старые копии сайтов, тестовые поддомены — за пароль прокси или за белый список адресов. Запрос, остановленный на прокси, до PHP не доходит;
- Обновите первым самое открытое и самое старое: публичный Apache с модулем PHP и ветки без поддержки — раньше, чем основной сайт за кэшем;
- Не трогайте
memory_limit«на всякий случай». Совет поставить-1, чтобы PHP никогда не упирался в предел, на сервере без лимитов памяти у контейнеров превращается в риск уронить весь сервер одним тяжёлым запросом; - Включите в журнале nginx время ответа и настоящий адрес клиента, если ещё не включили. Без них после инцидента вы не узнаете ни что было, ни откуда.
Что видно в журналах прямо сейчас
Волна заметна даже на маленьком сайте. На моём личном блоге за сутки — пачки по 400 запросов в час к несуществующим PHP-файлам: wp-conflg.php, /wp-content/plugins/<случайное имя>/up.php, .env в двух десятках каталогов. На одном из рабочих сайтов число запросов к несуществующим .php выросло за три дня с 54 до 2500 в сутки.
Важно понимать, что это такое. Это не сама атака PHP2Shell, а сканеры, которые ищут уже взломанные сайты: перебирают типичные имена чужих файлов в надежде, что какой-то из них откликнется. Все такие запросы у меня получили 404 или 403. Но всплеск именно сейчас — хороший повод проверить три вещи:
- выполнение PHP в каталоге загрузок запрещено на уровне nginx: запрос к
/wp-content/uploads/любой.phpдолжен получать 403, а не 404 и тем более не 200; - в журнале PHP нет всплеска падений рабочих процессов (
SIGSEGV,exited on signal) и сообщенийAllowed memory size … exhaustedна запросах, которые обычно столько памяти не просят; - в журнале nginx нет необъяснимых 502 — это то, как падение рабочего процесса выглядит снаружи.
# падения и нехватка памяти в журнале PHP за неделю
docker logs --since 168h site-php-1 2>&1 | grep -ciE 'SIGSEGV|exited on signal|Allowed memory size'
# 502/504 в журнале nginx
docker logs --since 168h site-nginx-1 2>&1 | grep -cE '" 50[24] '
# php в загрузках должен быть закрыт
curl -so /dev/null -w '%{http_code}\n' https://example.ru/wp-content/uploads/test.php # ждём 403
Нули в первых двух командах не доказывают, что атаки не было, но их отсутствие — хороший знак. Ненулевое значение — повод разобраться с каждой строкой до того, как вы решите, что сервер чист.
Чтобы одна дыра не стала катастрофой
Обновление закрывает эту уязвимость, но не следующую. Критические ошибки в интерпретаторах, веб-серверах и популярных плагинах выходят несколько раз в год, и между публикацией и вашим обновлением всегда есть окно. Поэтому вопрос владельца сайта не только «закрыта ли дыра», но и «что будет, если в следующий раз я не успею». Ответ на него закладывается заранее, и вот из чего он состоит.
Изоляция: один сайт — одна клетка
- каждый сайт — в своём контейнере или под своим системным пользователем, со своими файлами. PHP одного сайта не должен видеть каталоги соседей;
- сокет Docker не монтируется в контейнеры сайтов никогда. Доступ к нему равен правам root на всём сервере;
- контейнеры сайтов не сидят в одной сети с чужими базами и служебными сервисами: каждому сайту — своя сеть, к прокси — отдельная;
- задайте контейнерам лимиты памяти и процессора — тогда один сайт не уронит остальные, что бы с ним ни происходило.
База данных: только своё и только нужное
- у каждого сайта свой пользователь базы с правами только на свою базу — не
rootи не общий на все проекты; - база не принимает подключения из интернета: порт 3306 наружу закрыт;
- данные, которые не нужны сайту для работы (старые выгрузки, архивы заявок), не хранятся в его базе.
Секреты: считайте их скомпрометированными
Если сайт какое-то время работал на уязвимой версии и был открыт в интернет, после обновления смените всё, что лежит в его настройках:
- пароль пользователя базы данных и ключи
AUTH_KEY/SALTвwp-config.php(смена ключей разлогинит всех — это и нужно); - пароли почтовых ящиков, через которые сайт отправляет письма;
- токены API: платёжные системы, CRM, мессенджеры, обмен с учётной системой;
- пароли администраторов сайта — хотя бы у тех, у кого они простые или давно не менялись.
Хорошая привычка, которая окупается именно в такие дни: держать список всех секретов сайта в одном месте с пометкой, где каждый меняется. Тогда смена занимает полчаса, а не день поисков.
Код только для чтения
- файлы сайта принадлежат не тому пользователю, от которого работает PHP, — тогда PHP может их выполнять, но не менять;
- писать PHP разрешено только туда, куда действительно нужно: загрузки, кэш. И в этих каталогах выполнение PHP запрещено на уровне веб-сервера;
- обновления плагинов и ядра ставятся при выкатке, а не из админки на живом сервере, — тогда права на запись коду не нужны вовсе.
Резервные копии, которым можно верить
- копии лежат вне сервера и сервер не может их удалить или перезаписать: ключ доступа у копий — только на запись новых;
- несколько точек во времени: если проблему заметят через неделю, вчерашняя копия уже не поможет;
- восстановление проверено на деле хотя бы раз — копия, из которой никто не пробовал подняться, это надежда, а не копия.
Наблюдение: заметить раньше клиента
- контроль изменений в файлах кода: появился новый
.phpили изменился существующий вне выкатки — сигнал; - всплески падений PHP, ответов 502 и нехватки памяти в журналах;
- исходящий трафик сервера: сайт, который вдруг начал много отправлять наружу, — повод разобраться;
- новые пользователи с правами администратора в WordPress;
- уведомления о сроках сертификатов и версиях ПО — чтобы о следующем выпуске безопасности узнать в день выхода, а не из журналов.
Чек-лист
- перечислить все места, где работает PHP: контейнеры, хостинг, панели, старые проекты;
- после обновления — сменить секреты сайтов, которые работали на уязвимой версии;
- у каждого — версия работающего процесса, а не пакета;
- всё, что ниже 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, — в план обновления; mod_php и ветки без поддержки — первыми;
- убедиться, что источник обновлений вообще выпускает новые версии для вашей системы;
- обновить, перезапустить, проверить версию у работающего процесса;
- служебные панели — за пароль прокси или убрать из интернета;
- посмотреть журналы: падения процессов, нехватка памяти, 502.
Не уверены, какая версия PHP у вас реально работает?
Пришлите доступ или вывод команды из статьи — проверю все сайты, составлю план обновления и помогу перейти на исправленную версию без простоя.
// загружаю комментарии…