21 октября 2026 команда PHP закрыла две критические уязвимости в самом движке: CVE-2026-109618 и CVE-2026-109619, обе ведут к удалённому выполнению кода (RCE). Исследователи назвали связку PHP2Shell. Оценка по NVD — CVSS 9.8 из 10, куда выше не бывает.
Самое неприятное: дыра не в чьём-то кривом коде, а в интерпретаторе. Значит, под ударом любой сайт на уязвимой версии PHP — и WordPress, и самописный, и любой фреймворк. Починка одна: обновить PHP до патченной версии в своей ветке и перезапустить php-fpm. Ниже — кто в риске, как за полминуты проверить все свои серверы и как закрыть.
Кто в риске
Обе CVE затрагивают одни и те же ветки. Уязвимо всё, что ниже фикса в своей ветке:
| Ветка PHP | Уязвимы | Исправлено в |
|---|---|---|
| 8.5 | < 8.5.12 | 8.5.12 |
| 8.4 | < 8.4.27 | 8.4.27 |
| 8.3 | < 8.3.36 | 8.3.36 |
| 8.2 | < 8.2.35 | 8.2.35 |
| 8.1 и старше | всё | нет фикса — ветка снята с поддержки |
Ветки 8.1 и старше уже не получают даже секьюрити-патчей, поэтому официального исправления для них не будет — единственный выход переехать на поддерживаемую 8.3 или 8.4. Если вы сидите на пакетах из репозитория дистрибутива (Debian, Ubuntu, RHEL), номер версии может отличаться — там фикс приезжает бекпортом в текущий номер, ориентируйтесь на дату обновления пакета, а не на upstream-версию.
Что это за уязвимости
Если коротко — две ошибки работы с памятью в движке PHP (классы CWE-416 use-after-free и CWE-824 обращение к неинициализированной памяти). По описанию в advisory:
- CVE-2026-109618. PHP использует
longjmp()при нехватке памяти (OOM), чтобы оборвать обработку запроса. Некоторые участки кода при этом оставляют память запроса в недоинициализированном состоянии, а последующая очистка её читает. При определённых условиях это приводит к RCE. - CVE-2026-109619. У PHP есть глобальное состояние, которое временно ссылается на память запроса. Если оно не сбрасывается корректно, то начинает указывать на уже освобождённую память, и следующие запросы читают чужое — тоже вплоть до RCE.
Деталей эксплуатации ни я, ни статья не даём — это не то, что нужно админу. Админу важны три вещи, и все три неприятные. Первое: атака идёт по сети без аутентификации (вектор AV:N/PR:N в CVSS). Второе: страдает сам интерпретатор, то есть никакой аудит вашего PHP-кода от этого не спасает. Третье: флаг AT:P (Attack Requirements: Present) говорит, что надёжная эксплуатация требует определённых условий — но на 9.8 это влияет слабо, рассчитывать на «авось» не стоит.
Как понять, в риске ли вы
Самый быстрый способ — посмотреть версию на самом сервере:
php -v php-fpm -v # или php-fpm8.3 -v, смотря как назван
Сравните с таблицей выше. Важный нюанс: CLI и FPM — это часто разные сборки, и обновить надо именно тот SAPI, через который работает сайт (обычно FPM). Проверяйте оба.
Когда серверов больше одного, руками проверять долго. Я написал маленький скрипт-инвентаризатор. Он никуда не стучится и ничего не эксплуатирует — только читает версии PHP на том же хосте (бинарники, пакеты дистрибутива и запущенные контейнеры) и сравнивает с патченной версией. Годится для cron и Zabbix: код возврата 2, если найдена уязвимая версия.
#!/usr/bin/env bash
# check-php-cve-2026-109618.sh — в риске ли PHP на этом хосте.
# Только читает локальные версии, никуда не стучится.
# Коды: 0 — всё патчено, 2 — есть уязвимые, 1 — PHP не найден.
set -u
declare -A FIXED=( [8.2]="8.2.35" [8.3]="8.3.36" [8.4]="8.4.27" [8.5]="8.5.12" )
RED=$'\033[31m'; GRN=$'\033[32m'; YEL=$'\033[33m'; DIM=$'\033[2m'; RST=$'\033[0m'
[ -t 1 ] || { RED=; GRN=; YEL=; DIM=; RST=; }
found=0; risk=0
extract() { grep -oE '[0-9]+\.[0-9]+\.[0-9]+' <<<"$1" | head -n1; }
verdict() {
local ver="$1" src="$2"; found=1; ver="${ver%%-*}"
local branch="${ver%.*}" major="${ver%%.*}" fix="${FIXED[$branch]:-}"
if [ -z "$fix" ]; then
if [ "$major" -lt 8 ] 2>/dev/null || { [ "$major" -eq 8 ] && [ "${branch#*.}" -lt 2 ]; } 2>/dev/null; then
risk=1; printf '%s[РИСК]%s %-16s %sветка %s снята с поддержки, переходите на 8.3/8.4%s\n' "$RED" "$RST" "$ver" "$DIM" "$branch" "$RST"
else
printf '%s[?]%s %-16s %sнеизвестная ветка %s, сверьте вручную%s\n' "$YEL" "$RST" "$ver" "$DIM" "$branch" "$RST"
fi
printf ' %s%s%s\n' "$DIM" "$src" "$RST"; return
fi
local lowest; lowest="$(printf '%s\n%s\n' "$ver" "$fix" | sort -V | head -n1)"
if [ "$ver" = "$fix" ] || { [ "$lowest" = "$fix" ] && [ "$ver" != "$fix" ]; }; then
printf '%s[OK]%s %-16s %s>= %s%s\n' "$GRN" "$RST" "$ver" "$DIM" "$fix" "$RST"
else
risk=1; printf '%s[РИСК]%s %-16s %sобновите до %s+ в ветке %s%s\n' "$RED" "$RST" "$ver" "$DIM" "$fix" "$branch" "$RST"
fi
printf ' %s%s%s\n' "$DIM" "$src" "$RST"
}
echo "== Проверка PHP на CVE-2026-109618 / CVE-2026-109619 =="; echo
# 1) бинарники в PATH
for name in php php8.2 php8.3 php8.4 php8.5 php-fpm php-fpm8.2 php-fpm8.3 php-fpm8.4 php-fpm8.5; do
bin="$(command -v "$name" 2>/dev/null)" || continue
v="$(extract "$("$bin" -v 2>/dev/null | head -n1)")"
[ -n "$v" ] && verdict "$v" "бинарник: $bin"
done
# 2) пакеты дистрибутива
if command -v dpkg-query >/dev/null 2>&1; then
while read -r pkg ver; do
[ -n "$ver" ] || continue
v="$(extract "$ver")"; [ -n "$v" ] && verdict "$v" "пакет: $pkg"
done < <(dpkg-query -W -f='${Package} ${Version}\n' 'php*-common' 'php-common' 2>/dev/null)
elif command -v rpm >/dev/null 2>&1; then
while read -r line; do
v="$(extract "$line")"; [ -n "$v" ] && verdict "$v" "пакет: $line"
done < <(rpm -q php php-cli php-fpm 2>/dev/null | grep -v 'not installed')
fi
# 3) запущенные Docker-контейнеры
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
while read -r cid cname; do
[ -n "$cid" ] || continue
v="$(extract "$(docker exec "$cid" php -v 2>/dev/null | head -n1)")"
[ -n "$v" ] && verdict "$v" "контейнер: $cname"
done < <(docker ps --format '{{.ID}} {{.Names}}' 2>/dev/null)
fi
echo
if [ "$found" -eq 0 ]; then echo "PHP на этом хосте не найден."; exit 1
elif [ "$risk" -eq 1 ]; then echo "${RED}Итог: есть уязвимые версии. Обновите PHP и перезапустите fpm.${RST}"; exit 2
else echo "${GRN}Итог: все найденные версии пропатчены.${RST}"; exit 0; fi
Запуск:
chmod +x check-php-cve-2026-109618.sh ./check-php-cve-2026-109618.sh
Если на хосте крутится сборка не из веток 8.2–8.5 (например, собранная вручную), скрипт честно пометит её [?] и попросит сверить вручную — ложного «всё чисто» не будет.
Как закрыть
Лечение одно — обновить PHP и перезапустить то, что его держит в памяти. Обновление пакета само по себе не перезапускает php-fpm — старый процесс останется уязвимым в памяти, пока его не рестартовать.
Debian / Ubuntu:
sudo apt update sudo apt install --only-upgrade php-cli php-fpm php-common sudo systemctl restart php8.3-fpm # подставьте свою версию fpm
RHEL / AlmaLinux / Rocky:
sudo dnf update php php-cli php-fpm sudo systemctl restart php-fpm
Если PHP в Docker — обновляется не на хосте, а в образе. Поднимите тег базового образа до патченной версии (например php:8.3.36-fpm), пересоберите и пересоздайте контейнеры:
docker compose pull docker compose up -d --build
После обновления прогоните скрипт ещё раз — он должен показать сплошь [OK].
Если обновиться прямо сейчас нельзя
Честно: полноценной замены патчу нет, уязвимость в самом движке. Но пока окно обслуживания не наступило, разумно снизить риск: держать сайт за WAF, ограничить доступ к админкам по белому списку IP, убрать наружу всё, что можно убрать, и внимательно смотреть логи на аномальные падения и перезапуски fpm-воркеров. Всё это не лечит, а только тянет время до нормального обновления.
Итог
Две критические RCE в самом PHP, CVSS 9.8, без аутентификации и независимо от вашего кода. Порядок действий простой:
- Прогнать скрипт на всех серверах, где есть PHP — и на веб-серверах, и там, где PHP только в CLI/cron.
- Проверить оба SAPI: и CLI (
php), и FPM — они могут быть разных версий. - Не забыть про Docker-образы — там PHP живёт отдельно от хоста.
- Обновить до фикса в своей ветке: 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
- Перезапустить php-fpm и веб-сервер, иначе старый процесс останется в памяти.
- Ветки 8.1 и старше — планировать переезд на поддерживаемую, фикса для них не будет.
- Прогнать скрипт повторно и убедиться, что всюду
[OK].
Удобно закинуть скрипт в cron или Zabbix и ловить код возврата 2 — тогда следующая такая дыра не застанет врасплох.


