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 явно — это и обновит образ, и сбросит кэш.

Пока обновление готовится

Переход на новую базу образа — это не пять минут: другие пути к конфигам, другой пользователь, другой набор расширений, и всё это надо проверить на тестовой копии. На это время есть несколько мер, которые не закрывают уязвимость, но сужают круг тех, кто до неё дотянется:

  1. Уберите из интернета то, чему там не место. Служебные панели, старые копии сайтов, тестовые поддомены — за пароль прокси или за белый список адресов. Запрос, остановленный на прокси, до PHP не доходит;
  2. Обновите первым самое открытое и самое старое: публичный Apache с модулем PHP и ветки без поддержки — раньше, чем основной сайт за кэшем;
  3. Не трогайте memory_limit «на всякий случай». Совет поставить -1, чтобы PHP никогда не упирался в предел, на сервере без лимитов памяти у контейнеров превращается в риск уронить весь сервер одним тяжёлым запросом;
  4. Включите в журнале 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;
  • уведомления о сроках сертификатов и версиях ПО — чтобы о следующем выпуске безопасности узнать в день выхода, а не из журналов.

Чек-лист

  1. перечислить все места, где работает PHP: контейнеры, хостинг, панели, старые проекты;
  2. после обновления — сменить секреты сайтов, которые работали на уязвимой версии;
  3. у каждого — версия работающего процесса, а не пакета;
  4. всё, что ниже 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, — в план обновления; mod_php и ветки без поддержки — первыми;
  5. убедиться, что источник обновлений вообще выпускает новые версии для вашей системы;
  6. обновить, перезапустить, проверить версию у работающего процесса;
  7. служебные панели — за пароль прокси или убрать из интернета;
  8. посмотреть журналы: падения процессов, нехватка памяти, 502.

Не уверены, какая версия PHP у вас реально работает?

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

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.