CVE-2026-109618 & CVE-2026-109619 | Last updated: October 23, 2026
Step 1: Find vulnerable instances
PHP2Shell has several attack paths that depend on the server API (mod_php or PHP-FPM), memory configuration and application code. Version checks alone fail in both directions: they miss hosts that strip the X-Powered-By header, and they flag Linux LTS packages that carry a backported fix under the old version string.
External exposure
Run the free PHP2Shell pentest by Plainsea against your internet-facing endpoints. Instead of reading banners, Plainsea's agentic testing identifies exposed PHP environments and checks whether each endpoint is actually vulnerable, with safety controls that keep the test from crashing production workers.
Internal Linux host audit
php -v on the command line is not enough. Long-running PHP-FPM and Apache workers keep the pre-patch binary in memory until they are restarted. These commands list every installed PHP binary, find processes still running deleted (replaced) PHP code, and check for distribution backports:
# 1. List every PHP binary on the host with its version
find / \( -path /proc -o -path /sys -o -path /dev \) -prune -o -type f \
\( -name 'php' -o -name 'php[0-9]*' -o -name 'php-fpm*' -o -name 'php-cgi*' \) \
-perm -u+x -print 2>/dev/null |
while read -r bin; do printf '%s: ' "$bin"; "$bin" -v 2>/dev/null | head -n1; done
# 2. Processes still mapping deleted (pre-patch) PHP binaries or libphp
sudo grep -lE '(php|libphp).*\(deleted\)' /proc/[0-9]*/maps 2>/dev/null |
cut -d/ -f3 | xargs -r ps -o pid,user,cmd -p
# Any process listed here has NOT been restarted since patching and is still exposed.
# 3a. Debian/Ubuntu: check the versioned package changelogs for the fix
zgrep -H -E 'CVE-2026-10961[89]' \
/usr/share/doc/php*-common/changelog.Debian.gz 2>/dev/null
# 3b. RHEL/Rocky/AlmaLinux
rpm -q --changelog php-common 2>/dev/null | grep -E 'CVE-2026-10961[89]'
Note: No output from step 3 means either the backport is not installed or your vendor has not published it yet. Check your distribution's security tracker before concluding either way.
Kubernetes
Check every container in every pod, not just the first one:
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" "}{range .spec.containers[*]}{.name}{","}{end}{"\n"}{end}' |
while read -r ns pod containers; do
for c in ${containers//,/ }; do
ver=$(kubectl exec -n "$ns" "$pod" -c "$c" -- php -r 'echo PHP_VERSION;' 2>/dev/null)
[ -n "$ver" ] && echo "$ns/$pod [$c]: PHP $ver"
done
done
This needs pods/exec permission and misses distroless images, which have no php binary to call. For full coverage, list the running images and scan them with your container scanner once its database includes both CVEs:
kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.image}{"\n"}{end}' | sort -u
Step 2: Patch runtimes and restart workers
Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or your distribution's patched package, then restart every process that runs PHP. Restarting nginx is not needed; it does not load PHP.
# Debian / Ubuntu
sudo apt-get update
sudo apt-get install --only-upgrade -y $(dpkg-query -W -f='${binary:Package}\n' 'php*' 'libapache2-mod-php*' 2>/dev/null)
sudo systemctl restart 'php*-fpm.service' # every FPM pool
sudo systemctl restart apache2 # only if Apache runs mod_php
# RHEL / AlmaLinux / Rocky Linux / Amazon Linux
sudo dnf upgrade -y 'php*'
sudo systemctl restart php-fpm
sudo systemctl restart httpd # only if Apache runs mod_php
Then run check 2 from Step 1 again. It should return nothing.
For Docker and Kubernetes, pin the patched base image, rebuild with --pull --no-cache, and roll the deployments (kubectl rollout restart deployment/<name>). The check below fails the build if PHP is older than the fix for its branch, and keeps passing when later patch releases ship:
FROM php:8.4.27-fpm-bookworm
# Fail the build if PHP is older than the October 21, 2026 fix for its branch
RUN php -r 'exit(version_compare(PHP_VERSION, ["8.5"=>"8.5.12","8.4"=>"8.4.27","8.3"=>"8.3.36","8.2"=>"8.2.35"][PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION] ?? "99", ">=") ? 0 : 1);'
Step 3: Temporary mitigations if you cannot patch yet
Updating is the only complete fix. The PHP security team states that the measures below reduce exposure to specific attack paths but do not make an installation safe. Use them only to cover the gap until a maintenance window.
A. PHP configuration
Set these administratively so applications cannot override them with ini_set() or .user.ini, then restart every worker.
; e.g. /etc/php/8.x/fpm/pool.d/www.conf
php_admin_value[memory_limit] = -1 ; see note below
php_admin_value[post_max_size] = 8M ; 32M if uploads are required
php_admin_value[upload_max_filesize] = 7M ; keep below post_max_size
php_admin_flag[session.upload_progress.enabled] = off
php_admin_flag[fatal_error_backtraces] = off ; PHP 8.5 only
; Not from the PHP advisory: recycle workers sooner to shorten stale-state lifetime
pm.max_requests = 50
APACHE (MOD_PHP, IN A VIRTUALHOST OR DIRECTORY BLOCK, NOT .HTACCESS)
php_admin_value memory_limit -1
php_admin_value post_max_size 8M
php_admin_value upload_max_filesize 7M
php_admin_flag session.upload_progress.enabled off
php_admin_flag fatal_error_backtraces off
-
memory_limit = -1 removes the PHP-controlled out-of-memory failure, but a real allocation failure can still reach the vulnerable path. Put a container or cgroup memory limit around the workers as the availability boundary. Do not use RLIMIT_AS or RLIMIT_DATA: they can trigger the very failure you are trying to avoid.
-
Disabling upload progress blocks the confirmed session attack path for CVE-2026-109619, not every variant. Upload progress bars will stop working.
-
Fix what the attacker can reach: remove invalid or deprecated per-request settings that emit diagnostics before initialization, and fix attacker-reachable fatal errors and timeouts. Hiding warnings with error_reporting, display_errors or @ does not help.
B. Reverse proxy limits (nginx)
Cap request body size before traffic reaches PHP, and reject multipart/form-data outside the endpoints that genuinely accept uploads:
http {
# 8m if the site takes no uploads, 32m if it does
client_max_body_size 8m;
# Allow multipart only on known upload endpoints; list allowlist entries first
map "$content_type:$uri" $deny_multipart {
default 0;
"~*^multipart/form-data.*:/(api/v1/upload|wp-admin/async-upload\.php)$" 0;
"~*^multipart/form-data" 1;
}
server {
listen 443 ssl;
server_name example.com;
if ($deny_multipart) {
return 403;
}
}
}
Caution: Blocking multipart breaks every form that takes a file, including CMS media uploads and contact forms with attachments. Build the allowlist from your access logs before enforcing it.
Step 4: Hunt for exploitation and verify the fix
Patching closes the hole; it does not remove anything an attacker already planted. Check for signs of exploitation before you declare a host clean:
# 1. Out-of-memory spikes in PHP error logs (CVE-2026-109618 attempts)
# Noisy on its own: look for bursts from a single source. Disappears once memory_limit = -1.
grep -rh "Allowed memory size of .* bytes exhausted" /var/log/php* /var/log/apache2/ /var/log/httpd/ 2>/dev/null
# 2. Known PHP2Shell scanner and PoC user agents in access logs (indicators published by VulnCheck)
grep -rhE 'php2shell-(scan|verify|poc)' /var/log/apache2/ /var/log/httpd/ /var/log/nginx/ 2>/dev/null
# 3. Session files carrying planted PHP code (confirm your path with session.save_path)
sudo grep -rlE 'PWNED::|<\?php' /var/lib/php/sessions/ /tmp/sess_* 2>/dev/null
# 4. Web server user spawning shells or network tools
# Requires an auditd execve rule; it records from the moment you add it, not retroactively.
sudo auditctl -a always,exit -F arch=b64 -S execve -F euid=33 -k web_exec # 33 = www-data on Debian/Ubuntu
sudo ausearch -k web_exec -i | grep -E 'exe=".*/(sh|bash|dash|curl|wget|nc|ncat|python3?|perl)"'
Access logs do not record POST bodies, so searching them for PHP_SESSION_UPLOAD_PROGRESS finds nothing. If you log request bodies at a WAF, look for multipart POSTs carrying that field, the boundary ------p2s9rdme2st6guc9g0rafic and the upload filename p2s.txt.
If you find a hit: treat the host as compromised, not just vulnerable. Isolate it, preserve logs and memory, and rotate every secret the PHP process could read.
Once patches or mitigations are in place, rerun the free PHP2Shell pentest by Plainsea. The retest confirms the exposure is closed on every endpoint it reaches and gives you a dated result for your audit trail. Behind a load balancer, target each node directly; otherwise you have only tested whichever node answered.



