On 21 October 2026, PHP fixed CVE-2026-109618 and CVE-2026-109619 (CVSS 4.0 score 9.2), the remote code execution pair known as PHP2Shell. Both bugs sit in the PHP engine itself, not in a framework or a plugin. Any PHP that serves web requests and predates 8.5.12, 8.4.27, 8.3.36 or 8.2.35 is affected (the php2shell.com researchers trace affected releases back to PHP 5.4), and the attacks need no login, only HTTP requests to a PHP endpoint.
This page answers the two questions that matter on your servers: is this PHP vulnerable, and is the fix actually running. We cover the fixed version for every common way PHP is installed, a read-only checker script that audits every PHP CLI and FPM binary in the usual paths on a host, the three ways to prove a fix, and a post-patch audit checklist. All of it was tested on 21 and 22 October 2026 on Ubuntu 26.04, 24.04 and 22.04, Debian 13 and 12 (VM and containers) and Rocky Linux 10, 9 and 8 lab VMs, with SELinux enforcing on the Rocky boxes, so no step turns it off.
What PHP2Shell Exposes
The two CVEs are separate bugs with one root cause: PHP could jump out of a request in the middle of updating its own state, then trust that half-written state later. The fix (a commit titled “Avoid state corruption via bailout”) stops PHP from bailing out at those points and resets leftover state before the next request:
- CVE-2026-109618 (GHSA-w8hw-w9fx-j79w): running out of memory while PHP parses a multipart request body corrupts memory that request cleanup later uses.
- CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp): state from one request survives into the next request on the same worker. The confirmed paths are session upload progress and, on PHP 8.5,
fatal_error_backtraces.
The exposure depends on how PHP runs. PHP’s advisory rates Apache with mod_php the highest risk, because mod_php runs request shutdown even when request startup failed, so one anonymous request reaches the bug before your code runs. PHP-FPM is not a safe harbour either: PHP validated the session upload-progress attack against FPM, and its source review treats FPM as susceptible to the out-of-memory bug as well. The researchers at php2shell.com add a quieter problem, which is that the same bugs can leak memory from other requests, including passwords, tokens and API keys, without running any code.
On the day of release, CISA said it had not observed exploitation, while CERT-EU reported proofs of concept circulating. The CVE IDs began circulating the day before, with a wrong CVSS score of 10.0, and the PHP Foundation says no technical details leaked with them. Two things did slip out early: one of the reporting firms published a proof-of-concept video by mistake that evening, and Chainguard shipped container images carrying the fix more than six hours before the coordinated release. That is why proofs of concept were already circulating on release day. The PHP security team had still not identified a functional exploit when the fix shipped. Treat every internet-facing PHP server as a target until it is patched.
Fixed Versions by Platform
“Fixed” looks different depending on who built your PHP. Upstream, Debian, Red Hat’s supported streams, Amazon Linux, the Remi repository’s 8.2 to 8.5 streams, the sury.org packages, cPanel’s ea-php82 to ea-php85 and the official Docker images move to the new version number. Ubuntu, AlmaLinux’s own rebuilds, CloudLinux and every end-of-life backport (8.1, 8.0 and 7.4, whoever built it) keep the old number and change only the package release, so php -v cannot tell you anything there. This table combines our lab results with each vendor’s published status and repository contents as of 22 October 2026:
| Where PHP comes from | Fixed build | How to confirm | Source |
|---|---|---|---|
| php.net upstream | 8.5.12, 8.4.27, 8.3.36, 8.2.35 | version | PHP advisory |
| Ubuntu 26.04 / 24.04 / 22.04 | php8.5 8.5.4-0ubuntu1.5, php8.3 8.3.6-0ubuntu0.24.04.13, php8.1 8.1.2-1ubuntu2.28 | changelog | lab + USN-8948-1 |
| Debian 13 / 12 | php8.4 8.4.27-1~deb13u1, php8.2 8.2.35-1~deb12u1 | version | lab + DSA-6574-1, DLA-4848-1 |
| RHEL 10 / 9 / 8 AppStream | 10: php 8.3.36, php8.4 8.4.27; 9: php:8.3 8.3.36, php:8.2 8.2.35, php 8.0.30-8.el9_8.1; 8: php:8.2 8.2.35, php:7.4 7.4.33-8 | version; changelog for 8.0 and 7.4 | RHSA-2026:87070, 87071, 87072, 87073, 87079, 87080, 87151 |
| Rocky Linux 10 / 9 / 8 | the same builds as RHEL, in the repositories from 22 October | version; changelog for 8.0 and 7.4 | Rocky AppStream repository |
| AlmaLinux 10 / 9 / 8 | own .alma.1 backports that keep the version (for example 8.4.24-1.el10_2.alma.1, 8.0.30-8.el9_8.alma.1), plus Red Hat’s 8.3.36 and 8.2.35 rebuilds on 10 and 9 | changelog | AlmaLinux repository, package changelogs |
| Remi repository (EL 8, 9, 10) | 8.2.35 to 8.5.12; 8.1.34-6, 8.0.30-21, 7.4.33-31 backports | version or changelog | lab + Remi index |
| cPanel EasyApache 4 | ea-php82 to 85 bumped; ea-php74/80/81 backported | changelog | EA4 change log 25.91 |
| DirectAdmin CustomBuild | 8.2 to 8.5 bumped; 8.1 and older marked with * | da build versions | DirectAdmin forum (staff) |
| CloudLinux ea-php and alt-php | backports that keep the old version | changelog | CloudLinux status page |
| Docker official php images | php:8.x.y tags for the fixed releases | version | docker-library/php |
| Amazon Linux 2023 | php8.2 8.2.35, php8.3 8.3.36, php8.4 8.4.27, php8.5 8.5.12 (-1.amzn2023.0.1); php8.1 no fix planned | version | ALAS2023-2026-3301 to 3304 |
Two gaps remain, and both leave a server looking patched when it is not. Red Hat fixed only the streams it still supports. RHEL 9’s php:8.1 stream (8.1.32) and RHEL 8’s php:8.0 stream (8.0.30-1) had no erratum on 22 October, so on RHEL and Rocky a clean dnf upgrade on those streams changes nothing and the fix has to come from Remi. AlmaLinux is the exception. It shipped its own .alma.1 backports for both streams (8.1.32-1 and 8.0.30-1). And PHP 7.3 and older (5.6, 7.0 to 7.3) have no free fix from PHP or most vendors. DirectAdmin has since added patches for those branches, but if your stack is not on that list, the only options are a paid long-term support build or a PHP upgrade.
Audit Every PHP Binary With the Checker
A single host often carries more PHP than its owner remembers: the distro PHP, a Remi or sury build for one application, an SCL package for an old one, and a control panel’s own copies. We wrote a read-only bash script that finds them all and judges each one the way the table above does. It changes nothing on the server. For every PHP binary it finds, it checks, in order:
- whether the version is at or above the fixed upstream release for its branch
- whether the owning package’s changelog names CVE-2026-109618 or CVE-2026-109619
- whether a DirectAdmin build is newer than DirectAdmin’s PHP2Shell patch file
- whether the new
memory_hard_limit_percentsetting exists, as a last hint
It audits PHP-FPM binaries as well as the CLI, reading each one through -i because php-fpm refuses -r. That matters on the RHEL family, where the php-fpm package does not pull in php-cli, so an nginx server built with dnf install php-fpm may have no php command at all. Symlinks and wrapper scripts are resolved to the binary they run. It does not audit Apache’s mod_php module, the php-cgi or lsphp binaries, or NGINX Unit’s PHP module, and its restart check does not cover long-running Swoole or RoadRunner workers. Check those through their package or vendor.
It then lists any web or PHP process still running a replaced library and warns when Apache loads mod_php. Create the script:
sudo vim /usr/local/bin/php2shell-check
Paste the full script. It is the exact file we ran on the Rocky Linux 9 and Ubuntu 22.04 VMs and in a Debian 13 container on 22 October:
#!/usr/bin/env bash
# php2shell-check: read-only audit of every PHP build on a host for
# CVE-2026-109618 / CVE-2026-109619 (PHP2Shell). Changes nothing.
# computingforgeeks.com
set -u
FIXED="8.2.35 8.3.36 8.4.27 8.5.12"
CVE_RE='CVE-2026-10961[89]'
rc=0
audited=0
ver_ge() { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; }
fixed_for_branch() {
local b=$1 f
for f in $FIXED; do [ "${f%.*}" = "$b" ] && { echo "$f"; return; }; done
}
pkg_changelog_has_fix() {
local bin=$1 pkg
if command -v dpkg >/dev/null 2>&1 && pkg=$(dpkg -S "$bin" 2>/dev/null | head -1 | cut -d: -f1) && [ -n "$pkg" ]; then
zgrep -qE "$CVE_RE" "/usr/share/doc/$pkg/changelog.Debian.gz" 2>/dev/null && { echo "$pkg"; return 0; }
elif command -v rpm >/dev/null 2>&1 && pkg=$(rpm -qf "$bin" 2>/dev/null) && [ -n "$pkg" ]; then
rpm -q --changelog "$pkg" 2>/dev/null | grep -qE "$CVE_RE" && { echo "$pkg"; return 0; }
fi
return 1
}
directadmin_status() {
local bin=$1 b=$2 patch
patch=$(ls /usr/local/directadmin/custombuild/patches/php-"$b"/*cve-2026-109618* 2>/dev/null | head -1)
[ -n "$patch" ] || return 1
[ "$bin" -nt "$patch" ] && echo "built after $(basename "$patch")" || echo "older than $(basename "$patch"), rebuild"
}
candidates() {
{
command -v php
ls /usr/bin/php[0-9]* /usr/sbin/php-fpm* /usr/local/sbin/php-fpm 2>/dev/null
ls /opt/remi/php*/root/usr/bin/php /opt/remi/php*/root/usr/sbin/php-fpm 2>/dev/null
ls /opt/cpanel/ea-php*/root/usr/bin/php /opt/cpanel/ea-php*/root/usr/sbin/php-fpm 2>/dev/null
ls /opt/alt/php*/usr/bin/php /opt/alt/php*/usr/sbin/php-fpm 2>/dev/null
ls /usr/local/php*/bin/php /usr/local/php*/sbin/php-fpm* /opt/plesk/php/*/bin/php /opt/plesk/php/*/sbin/php-fpm 2>/dev/null
ls /usr/local/bin/php 2>/dev/null
} | while read -r p; do
[ -x "$p" ] || continue
p=$(readlink -f "$p")
[ -x "$p" ] || continue
case $p in
*fpm*) echo "$p" ;;
# wrappers and panel shims exec the real binary, which PHP_BINARY names
*) r=$("$p" -n -r 'echo PHP_BINARY;' 2>/dev/null); [ -x "$r" ] && readlink -f "$r" || echo "$p" ;;
esac
done | while read -r p; do
case ${p##*/} in php|php[0-9]|php[0-9][0-9]|php[0-9].[0-9]|php-fpm|php-fpm[0-9]*) echo "$p" ;; esac
done | sort -u
}
printf '%-48s %-10s %s\n' "BINARY" "VERSION" "STATUS"
for bin in $(candidates); do
# php-fpm rejects -r, so read everything from -i, which every SAPI binary supports
audited=$((audited + 1))
if ! info=$("$bin" -i 2>/dev/null); then
printf '%-48s %-10s %s\n' "$bin" "?" "SKIPPED (-i failed, run it by hand)"; rc=1; continue
fi
v=$(printf '%s\n' "$info" | awk -F' => ' '/^PHP Version => /{print $2; exit}')
v=${v%%[-+~]*}
if [ -z "$v" ]; then
printf '%-48s %-10s %s\n' "$bin" "?" "SKIPPED (no version in -i output)"; rc=1; continue
fi
b=$(echo "$v" | cut -d. -f1,2)
f=$(fixed_for_branch "$b")
if [ -n "$f" ] && ver_ge "$v" "$f"; then
st="FIXED (upstream $f or later)"
elif pkg=$(pkg_changelog_has_fix "$bin"); then
st="FIXED (vendor backport in $pkg)"
elif da=$(directadmin_status "$bin" "$b"); then
case $da in built*) st="LIKELY FIXED (DirectAdmin build newer than patch file)";; *) st="VULNERABLE (DirectAdmin: $da)"; rc=1;; esac
elif printf '%s\n' "$info" | grep -q '^memory_hard_limit_percent '; then
st="LIKELY FIXED (memory_hard_limit_percent present, no changelog proof)"
else
st="VULNERABLE"; rc=1
fi
up=$(printf '%s\n' "$info" | awk -F' => ' '/^session.upload_progress.enabled => /{print tolower($3); exit}')
printf '%-48s %-10s %s, ini upload_progress=%s\n' "$bin" "$v" "$st" "${up:-n/a}"
done
if [ "$audited" -eq 0 ]; then
echo "no PHP binary audited: none found in the usual paths, check where your PHP lives"
rc=2
fi
echo
echo "Running PHP processes still mapping replaced files (restart these):"
stale=0
for pid in $({ pgrep -x 'httpd|apache2|lsphp|frankenphp|php-cgi[0-9.]*'; pgrep -f '^php-fpm: master'; } 2>/dev/null | sort -u); do
if grep -qE ' /(usr|lib|opt).*\(deleted\)$' "/proc/$pid/maps" 2>/dev/null; then
printf ' pid %-8s %s\n' "$pid" "$(tr '\0' ' ' < /proc/$pid/cmdline | cut -c1-70)"
stale=1
fi
done
[ "$stale" = 0 ] && echo " none" || { [ "$rc" -eq 0 ] && rc=1; }
if command -v apachectl >/dev/null 2>&1 || command -v apache2ctl >/dev/null 2>&1; then
if { apachectl -M 2>/dev/null || apache2ctl -M 2>/dev/null; } | grep -qi 'php[0-9_]*_module'; then
echo
echo "Apache loads mod_php, the highest-risk SAPI: after any PHP update, restart Apache fully, not a graceful reload."
fi
fi
if [ "$(id -u)" -ne 0 ]; then
echo
echo "Not run as root: the restart check could not read every process. Run it again with sudo."
[ "$rc" -eq 0 ] && rc=1
fi
exit $rc
Make it executable and run it as root, so it can read every process’s memory map. Call it by its full path: sudo on the RHEL family uses a secure_path without /usr/local/bin, and a bare sudo php2shell-check on our Rocky Linux 9 VM failed with “sudo: php2shell-check: command not found”:
sudo chmod 755 /usr/local/bin/php2shell-check
sudo /usr/local/bin/php2shell-check
In a Debian 13 container still on the stock 8.4.24 build, it flagged both the CLI and the PHP-FPM binary and exited with status 1:
BINARY VERSION STATUS
/usr/bin/php8.4 8.4.24 VULNERABLE, ini upload_progress=on
/usr/sbin/php-fpm8.4 8.4.24 VULNERABLE, ini upload_progress=on
Running PHP processes still mapping replaced files (restart these):
none
The Rocky Linux 9 VM is the harder case. Its default PHP 8.0 now comes from the Rocky rebuild of the Red Hat 8.0.30-8.el9_8.1 backport, and a Remi PHP 8.1 software collection runs next to it. Both keep their old version numbers, yet all four binaries came back clean, each with the package whose changelog proves it, and the script exited 0:
BINARY VERSION STATUS
/opt/remi/php81/root/usr/bin/php 8.1.34 FIXED (vendor backport in php81-php-cli-8.1.34-6.el9.remi.x86_64), ini upload_progress=on
/opt/remi/php81/root/usr/sbin/php-fpm 8.1.34 FIXED (vendor backport in php81-php-fpm-8.1.34-6.el9.remi.x86_64), ini upload_progress=on
/usr/bin/php 8.0.30 FIXED (vendor backport in php-cli-8.0.30-8.el9_8.1.x86_64), ini upload_progress=on
/usr/sbin/php-fpm 8.0.30 FIXED (vendor backport in php-fpm-8.0.30-8.el9_8.1.x86_64), ini upload_progress=on
Running PHP processes still mapping replaced files (restart these):
none
We then removed php-cli and php81-php-cli from the same VM to mimic an FPM-only nginx box. With no php command left, the checker still audited both FPM builds:
BINARY VERSION STATUS
/opt/remi/php81/root/usr/sbin/php-fpm 8.1.34 FIXED (vendor backport in php81-php-fpm-8.1.34-6.el9.remi.x86_64), ini upload_progress=on
/usr/sbin/php-fpm 8.0.30 FIXED (vendor backport in php-fpm-8.0.30-8.el9_8.1.x86_64), ini upload_progress=on
Running PHP processes still mapping replaced files (restart these):
none
Read the result by its exit status, which drops straight into an Ansible task or a monitoring check:
- Exit 0 means every binary it found reads FIXED or LIKELY FIXED and no PHP or web server process maps a replaced file.
- Exit 1 means at least one binary is VULNERABLE, a binary was SKIPPED because
-ifailed, a process still needs a restart, or the script ran without root and could not read every process. - Exit 2 means it found no PHP binary at all, which on a web server means PHP lives in a path the script does not know, never that the host is clean.
Two columns need care. The ini upload_progress value comes from that binary’s own php.ini tree, so it misses a pool-level php_admin_value, and on the end-of-life backports it reads on even though the fix turns the feature off in code. The restart check flags any deleted library a running process still maps, so an openssl or glibc update trips it too, and restarting that service clears it either way. On DirectAdmin the script only compares the build time of the binary with DirectAdmin’s patch file, which is why it reports LIKELY FIXED there. The proof is in the build itself, as the DirectAdmin patch guide shows: the build log has no FAILED or “failed to apply” lines, and da build versions no longer offers an update for that slot.
Three Ways to Prove the Fix
A version number alone misleads on half the platforms in the table, so we use three kinds of evidence and trust the strongest one available on each build:
- The version is at or above 8.5.12, 8.4.27, 8.3.36 or 8.2.35 on an upstream-numbered build.
- The package changelog names CVE-2026-109618 or CVE-2026-109619 on a backported build.
- The web server reports the patched version to a request from another machine.
The changelog check is the one that settles a backport. On Debian and Ubuntu it reads the local file, and on the RHEL family it asks rpm:
zgrep -E 'CVE-2026-10961[89]' /usr/share/doc/php8.3-fpm/changelog.Debian.gz
rpm -q --changelog php-fpm | grep -E 'CVE-2026-10961[89]'
Remi’s upstream-numbered builds (8.2.35 and newer) only say “Update to 8.3.36” in the changelog, so an empty grep there is normal and the version is your proof. The reverse holds for backports, including Red Hat’s 8.0 and 7.4 errata and AlmaLinux’s .alma.1 rebuilds: the version stays put and the changelog carries the evidence.
The fixed engine also gained a new ini setting, memory_hard_limit_percent, which makes a handy runtime hint. On a vulnerable build, ini_get returns false because the setting does not exist. On a full fix it returns an empty string. We saw the empty string on Ubuntu’s backported 8.5 and 8.3, Debian’s 8.4.27 and Remi’s 8.3.36. The end-of-life backports (Ubuntu’s 8.1, Remi’s 8.0 and 8.1) do not add it, so a false there does not mean vulnerable, and that is why the checker uses this only as a last resort.
For the outside view, the RHEL family leaves expose_php on, while Ubuntu sets it Off in the FPM and Apache php.ini (only the CLI file has it On, as our 22.04 VM showed). On the RHEL family, a plain header request from another host shows which build answered. Against our patched Rocky Linux 10 server it returned X-Powered-By: PHP/8.3.36. Turn expose_php off afterwards, because scanners read that header too.
Patch Guides by Platform
Each platform below has its own tested walkthrough, because the commands, the restart behaviour and the proof all differ:
- Ubuntu 26.04, 24.04 and 22.04: one
apt-get install --only-upgradeline; the version string stays, the changelog proves it. - Debian 13 and 12: the security archive ships the upstream release, so
php -vis valid. - RHEL, Rocky Linux and AlmaLinux 10, 9 and 8: AppStream errata for the supported streams, Remi for the branches Red Hat skipped.
- cPanel and WHM: ea-php packages, CloudLinux alt-php and Imunify360 rules.
- DirectAdmin: CustomBuild rebuilds, the starred backports and the PHP 8.1 build failure.
- End-of-life PHP 8.1, 8.0 and 7.4: applications such as WHMCS that cannot leave an old branch yet.
Containers follow their own rule. A running container never updates itself, so pull the fixed php image, rebuild your own images with docker build --pull, and recreate the containers. FrankenPHP and other software that bundles its own PHP needs a new release of that software, not an apt or dnf update.
When Patching Has to Wait
PHP’s position is blunt: updating is the only complete fix, and no configuration closes every path. If a server genuinely cannot take the update today, the published mitigations narrow the confirmed attack paths while you schedule it. The two that matter most are switching off session upload progress in the configuration PHP-FPM actually reads (not the CLI’s, and never through ini_set() or .user.ini), and blocking multipart requests at the web server on sites that accept no uploads. Every mitigation, tested on nginx, Apache and Cloudflare with the pitfalls we hit, is in the PHP2Shell mitigation guide.
Once patched, the upgrade changes one behaviour you will see in logs. The fix adds a hard memory ceiling (by default twice memory_limit for limits up to 1G), and a PHP-FPM worker that crosses it ends with exit code 137 instead of failing gracefully. PHP calls _exit(137) itself, so the FPM log says “exited with code 137”, not “exited on signal 9”. We reproduced that and documented the log lines in the memory_hard_limit_percent explainer.
Post-Patch Audit Checklist
Run through this list on every PHP host before you mark it done. Each item is a condition that must hold, and the first four are the ones our lab VMs failed before the fix was complete:
- Every PHP binary on the host reports FIXED or LIKELY FIXED in
php2shell-check, including FPM-only, SCL, panel and second-branch builds, and the script exits 0 when run as root. - No PHP-FPM, Apache, LiteSpeed or FrankenPHP process still maps a deleted library.
- Apache with mod_php was fully restarted, not gracefully reloaded.
- The PHP-FPM build reports
session.upload_progress.enabledas Off throughphp-fpm8.X -ion Ubuntu and Debian (php-fpm8.3,php-fpm8.4and so on) orphp-fpm -ion the RHEL family, neverphp -i, and no pool file switches it back on withphp_admin_value. - Any interim
memory_limit = -1orpm.max_requests = 1has been reverted to a real value. - A request from another machine is answered by the patched build.
expose_phpis Off once you no longer need the header for verification.- Containers were rebuilt from the fixed base image and recreated, not just restarted.
- Servers on PHP 7.3 or older are offline, isolated behind the mitigations, or on a paid long-term support build, and have an upgrade date.
Item 4 is the one most often missed. On Ubuntu and Debian the CLI and PHP-FPM read different ini trees, so a shell test can show Off while every website still has the feature on.