▶Prefer to watch? This short video runs this page's main commands in a real lab, with on-screen captions and a synthetic voiceover (an AI voice trained on the author's own recording). Click to play, or watch it on YouTube.
php2shell: unauthenticated remote code execution in PHP itself
CVE-2026-109618 and CVE-2026-109619 CVSS v4.0 9.2 (Critical) each fixed in the PHP releases of October 21, 2026 CISA Known Exploited: October 26, 2026
Two bugs in the PHP engine let a remote visitor run commands as the web server user, with no login, before any of your application code runs. A password page or a CMS login does not help, because PHP trips over the request while it is still reading it. CISA lists both as exploited in the wild.
This page is for the machines the update does not reach: end-of-life distributions, and distributions whose vendor has not shipped the fix yet. It shows how to find out whether a box is exposed, how to look for signs of an attack, and how to reduce the risk by hand until you can patch.
Everything below was run in lab containers on 2026-11-07 (Ubuntu 20.04 with PHP 7.4 as the unpatched end-of-life box, Ubuntu 24.04 and Rocky Linux 9 as patched boxes). Lines marked "not run in the lab" are taken from the sources at the bottom.
A mitigation is a bandage, not a fix. The researchers say plainly that the settings below do not close the holes. Patch, or take the box off the internet.
Defensive notes only: no exploit code here. Only run the commands on machines you own or administer.
What it is, in plain English
PHP reads a whole web request (form fields, file uploads) before it runs a single line of your script. Both bugs live in that early stage, and in what PHP leaves behind afterwards.
CVE-2026-109618: when PHP runs out of memory while it is still unpacking a request, it tidies up its half-built data in an unsafe way. A request built to hit that moment corrupts memory. Researchers describe the trigger as one oversized form field name sent when the request is close to memory_limit.
CVE-2026-109619: PHP keeps some state from a failed request and a later request reads it. Two cases are confirmed by PHP: the fatal-error backtrace code (PHP 8.5 only) and the upload processing code, including session upload progress, which is switched ON by default and runs even if your site never starts a session.
Because it happens before your code runs, application logins, CMS plugins and "only admins can upload" rules give no protection. Authentication in a WAF or reverse proxy in front of PHP does, because the request never reaches PHP.
Impact: code execution as the web server user (www-data, apache, nginx). CISA lists Apache with mod_php as the highest risk, and also PHP-FPM, php-cgi, FrankenPHP and LiteSpeed. A public proof of concept exists, but it works reliably only on specific combinations of operating system, PHP version and web server. Do not read that as safe: scanning for it is already happening.
Researchers (Datadog, Qualys, Rapid7) provide extra detail on the mechanism. PHP's own advisory is the authority on what is confirmed. Where this page quotes a researcher's observation, it says so.
Who is affected
Upstream PHP: every release before 8.5.12, 8.4.27, 8.3.36 and 8.2.35 (the four fixed releases of October 21, 2026).
Older PHP: 8.1, 8.0, 7.4 and older get no new upstream release. PHP published minimal source backports for 7.4, 8.0 and 8.1; distributions may carry their own. CISA's advice for 8.1 and older: take them offline until updated.
Where PHP runs: Apache mod_php, PHP-FPM behind nginx or Apache, php-cgi, FrankenPHP, LiteSpeed (lsphp), and PHP bundled inside containers and applications. Not only the "PHP" package of the distribution.
Lower risk: command-line PHP that no web server calls (cron jobs, scripts), because the bugs are reached through web requests. Update it anyway.
Version numbers lie: Debian, Ubuntu, Red Hat and Rocky fix PHP by patching the old version, so "PHP 8.0.30" or "8.3.6" can be fully patched, and "8.2.30" can be vulnerable. Check the package changelog, not only php -v (see below).
Step 1: find every PHP on the machine
php -v # the COMMAND-LINE php only. The web server may use a different one
ls /etc/php/ 2>/dev/null # Debian/Ubuntu: one folder per installed version
dpkg -l | grep -E '^ii +(php|libapache2-mod-php|frankenphp)' # Debian/Ubuntu packages
rpm -qa | grep -iE '^(php|mod_php|lsphp|frankenphp)' # RHEL / Rocky / Alma / SUSE packages
ps -eo pid,user,args | grep -E 'php-fpm|php-cgi|lsphp|frankenphp|apache2|httpd' | grep -v grep
# what is actually running right now, and as which user
find / -xdev -type f \( -name 'php' -o -name 'php[0-9]*' -o -name 'php-fpm*' -o -name 'libphp*.so' -o -name frankenphp \) -perm /111 2>/dev/null
# PHP that came with an application or was built by hand
Containers: every image has its own PHP. Check each one (not run in the lab):
for c in $(docker ps -q); do echo "== $c"; docker exec $c php -v 2>/dev/null | head -1; done
Also look inside: control panels (Plesk ships its own PHP, with its own advisory), Docker images you pulled long ago, and "portable" PHP bundled in tools.
Step 2: is this PHP patched?
Compare with the fixed releases (8.5.12, 8.4.27, 8.3.36, 8.2.35) ONLY for PHP you built or downloaded from php.net. For a distribution package, read the changelog of the package you have installed:
Debian / Ubuntu:
zcat /usr/share/doc/php8.3-cli/changelog.Debian.gz | grep -m1 CVE-2026-109618 # no output = no fix in this package
apt changelog php8.3-cli | grep -c CVE-2026-109618 # same question, asks the archive (needs network)
Red Hat / Rocky / Alma:
rpm -q --changelog php-cli | grep -B1 CVE-2026-109618
sudo dnf updateinfo list --cve CVE-2026-109618 # which advisory fixes it and whether it is installed (not run in the lab)
Plesk, cPanel, Remi, Sury and other third-party builds: read their own advisory. The package name tells you who built it: dpkg -S $(which php-fpm8.3), rpm -qf /usr/sbin/php-fpm
What the lab showed:
Ubuntu 24.04 php8.3 8.3.6-0ubuntu0.24.04.13 changelog: "SECURITY UPDATE: Remote RCE via state corruption issue ... CVE-2026-109618_109619.patch" PATCHED
Rocky Linux 9 php-cli-8.0.30-8.el9_8.1 changelog: "Avoid state corruption via bailout CVE-2026-109618, CVE-2026-109619" PATCHED
Ubuntu 20.04 php7.4 7.4.3-4ubuntu2.29 changelog: no mention (grep prints nothing) NOT PATCHED (standard support has ended)
Notice that 8.3.6 and 8.0.30 look far older than the fixed 8.3.36 and 8.2.35, and are patched anyway.
From the Debian security tracker (not run in the lab): bookworm php8.2 is fixed in 8.2.35-1~deb12u1 and trixie php8.4 in 8.4.27-1~deb13u1. Other distributions: search their tracker for CVE-2026-109618.
A package that is updated but still running the old code: php-fpm and Apache keep the old PHP in memory until restarted.
ps -o lstart= -p $(pgrep -o php-fpm8.3) # when the running master started. If it is older than the package update, restart it (not run in the lab)
Step 3: what do the web workers really use?
The php.ini that php -i prints belongs to the command line. Each web server has its own folder (Debian: /etc/php/8.3/apache2, /fpm, /cli). Ask each one:
php-fpm8.3 -i | grep -E '^(session.upload_progress.enabled|memory_limit|post_max_size)' # PHP-FPM
php -c /etc/php/8.3/apache2/php.ini -i | grep -E '^(session.upload_progress.enabled|memory_limit)' # mod_php (reads the apache2 file)
The truth is what a request sees. Put a one-line page in the web root, request it through each web server, then DELETE it:
<?php header('Content-Type: text/plain'); printf("sapi=%s php=%s upload_progress=%s memory_limit=%s post_max_size=%s\n", php_sapi_name(), PHP_VERSION, ini_get('session.upload_progress.enabled'), ini_get('memory_limit'), ini_get('post_max_size'));
curl -s http://localhost/info.php # Apache mod_php
curl -s http://localhost:8080/info.php # nginx to PHP-FPM
Lab output of the unpatched Ubuntu 20.04 box before any change:
sapi=apache2handler php=7.4.3-4ubuntu2.29 upload_progress=1 memory_limit=128M post_max_size=8M
sapi=fpm-fcgi php=7.4.3-4ubuntu2.29 upload_progress=1 memory_limit=128M post_max_size=8M
upload_progress=1 means the CVE-2026-109619 upload path is open. Never leave a page like this on a production site: it is a map for anyone who finds it. Use a path nobody guesses, or run it from 127.0.0.1 only, and delete it.
Step 4: look for signs of an attack
No log line proves an attack, and a successful one may leave nothing but an HTTP 200. Look for three things.
1. PHP dying during request start-up. The location "Unknown on line 0" means PHP failed before any script ran:
grep -E 'Allowed memory size .* in Unknown on line 0' /var/log/apache2/error.log /var/log/nginx/error.log /var/log/php*-fpm.log /var/log/httpd/error_log /var/log/php-fpm/*.log 2>/dev/null
grep -E 'exit signal Segmentation fault|AH00052' /var/log/apache2/error.log /var/log/httpd/error_log 2>/dev/null # a worker that crashed
Lab (patched box, memory_limit lowered to 2M, an ordinary 3 MB form field, NOT an exploit) writes exactly this kind of line:
PHP Fatal error: Allowed memory size of 2097152 bytes exhausted (tried to allocate 2097152 bytes) in Unknown on line 0
Legitimate big uploads can write it too. A handful of them on one worker in a short time, from one address, followed by quiet, is what researchers report for an attempt (Datadog: a complete attack can leave only two to four such errors).
2. The strongest sign: a web worker starting a shell or a download tool. List every shell whose parent is the web server or PHP-FPM:
ps -eo pid,ppid,user,comm,args --no-headers | awk '{c[$1]=$4; p[$1]=$2; l[$1]=$0} END{for(i in p) if(c[p[i]] ~ /^(apache2|httpd|php-fpm|php-cgi|lsphp|frankenphp)/ && c[i] ~ /^(sh|bash|dash|nc|ncat|curl|wget)$/) print l[i]}'
Lab: a harmless test script that calls shell_exec("sleep 15") shows up at once, from Apache and from PHP-FPM:
66 39 www-data sh sh -c sleep 15
65 17 www-data sh sh -c sleep 15
Some applications run external programs legitimately (image resizing, PDF tools). Learn what is normal on your machine first.
For history, not a snapshot, use auditd on execve by the web user, or the SIGMA rule published by CISA (not run in the lab).
3. Files that should not be there: new or changed PHP files under the web root, and files owned by the web user outside upload folders.
find /var/www -name '*.php' -mtime -7 -ls # changed in the last 7 days
find /var/www -name '*.php' -user www-data -ls # PHP files the web user itself owns (it should not be able to write code)
Compare with your deployment (git status, a package manager verify, a backup). If a box was reachable by the internet since October 17 (AWS reported attempts before the release) and you find any of the above, treat it as compromised: isolate it, keep the logs, rebuild.
In the access logs the request is an ordinary POST. A WAF that records bodies can look for multipart parts named PHPSESSID or PHP_SESSION_UPLOAD_PROGRESS on sites that never use them, and for upload filenames containing <?php (researcher indicators, not run in the lab).
Manual mitigation: the PHP settings
Use when you cannot update: the distribution is end-of-life, or its vendor has no fixed package yet. Which setting covers which bug:
session.upload_progress.enabled = 0 CVE-2026-109619 switches off the upload progress tracking, one of the two confirmed paths
fatal_error_backtraces = 0 CVE-2026-109619 PHP 8.5 only. Older versions have no such setting and ignore the line
memory_limit = -1 CVE-2026-109618 PHP's own temporary advice: with no limit, a request cannot hit the "out of memory while starting" moment. Read the danger below
post_max_size, upload_max_filesize, max_file_uploads keep the "no limit" from becoming a denial of service (see below)
1. Write one drop-in file (Debian / Ubuntu layout, here for PHP 7.4; change the version to yours):
sudo tee /etc/php/7.4/mods-available/php2shell.ini >/dev/null <<'EOF'
; priority=99
; php2shell (CVE-2026-109618 / CVE-2026-109619) temporary mitigation. Added 2026-11-07. Remove after patching.
session.upload_progress.enabled = 0
memory_limit = -1
post_max_size = 8M
upload_max_filesize = 8M
max_file_uploads = 20
fatal_error_backtraces = 0
EOF
The "; priority=99" line matters: without it Debian's phpenmod links the file as 20-php2shell.ini and a later file can override it. Lab: a file called 99-php2shell.ini was linked as 20-99-php2shell.ini.
2. Enable it for every PHP SAPI of that version (cli, fpm, apache2 are all linked):
sudo phpenmod -v 7.4 php2shell
ls /etc/php/7.4/*/conf.d | grep php2shell # 99-php2shell.ini under apache2, cli and fpm
Red Hat / Rocky: the same lines in /etc/php.d/99-php2shell.ini (verified in the lab on Rocky 9 with php -i and php-fpm -i). Remi's packages use /etc/opt/remi/php83/php.d/ (not run in the lab)..
3. RESTART every worker. Nothing changes until then (lab: the values were still 1 and 128M after the file was linked):
sudo systemctl restart php7.4-fpm apache2 # Debian names. Red Hat: php-fpm and httpd
(apache2ctl graceful is fine too. PHP-FPM, mod_php under Apache, FrankenPHP and LiteSpeed each need their own restart.)
4. Verify through the web server, not only php -i (Step 3). Lab result after the restart:
sapi=apache2handler php=7.4.3-4ubuntu2.29 upload_progress=0 memory_limit=-1 post_max_size=8M
sapi=fpm-fcgi php=7.4.3-4ubuntu2.29 upload_progress=0 memory_limit=-1 post_max_size=8M
Danger of memory_limit = -1: one request can now use all the memory of the machine and the kernel kills something. That is why the size limits are there, and why you must add the container or cgroup memory limit below.
Do NOT use ulimit -v, RLIMIT_AS or RLIMIT_DATA (including LimitAS= in a systemd unit) to cap PHP's memory. PHP's advisory warns that they cause exactly the allocation failures the bug needs.
Manual mitigation: lock the settings so a site cannot undo them
On shared hosting and CMS servers, a site can often change PHP settings for itself. The lab proved it: with the drop-in in place and upload_progress=0, a one-line file brought it back.
PHP-FPM: echo 'session.upload_progress.enabled=1' > /var/www/html/.user.ini -> upload_progress=1 again a moment later
Apache: echo 'php_flag session.upload_progress.enabled on' > /var/www/html/.htaccess -> upload_progress=1 again
(The setting is allowed to be changed per directory.) Lock it where only the administrator can write:
PHP-FPM, in every pool (/etc/php/7.4/fpm/pool.d/www.conf, under the [www] line):
php_admin_value[session.upload_progress.enabled] = 0
Apache mod_php, in the server config (Debian: /etc/apache2/conf-available/php2shell.conf, then sudo a2enconf php2shell):
php_admin_flag session.upload_progress.enabled off
Restart both. Lab: with the lock in place the same .user.ini and .htaccess files changed nothing (upload_progress=0).
This keeps a site from switching the protection back on. It does not protect a site that is already compromised. Treat it as defence in depth.
Manual mitigation: keep big or hostile requests away from PHP
PHP's guidance lists limiting the decoded request body, container memory limits and limits at the front-end. These are the lab-tested ones.
Cap the request body in the web server so oversized requests are refused before PHP parses them:
Apache (/etc/apache2/conf-available/php2shell-limit.conf, a2enconf php2shell-limit):
LimitRequestBody 8388608
nginx (in the server block):
client_max_body_size 8m;
Lab, a 9 MB upload and a 5 MB upload: Apache 413 / 200 nginx 413 / 200
This can break legitimate big uploads. Raise it for the one path that needs it (a location block, a Directory block), not for the whole site. Both settings should also cover requests sent in chunks without a Content-Length (not run in the lab); check yours with a chunked test upload.
Take PHP off the open internet where you can. If only staff use the application, put it behind a VPN, an IP allow-list or basic auth at the proxy. A request refused by the proxy never reaches PHP:
nginx: allow 10.0.0.0/8; deny all; Lab: from an address not on the list, 403 and PHP never ran.
CISA's advice for PHP 8.1 and older, and for anything you cannot update quickly: take it offline until it is patched. If that is impossible, the order is: allow-list first, size caps second, PHP settings third.
Container memory ceiling (not run in the lab), so memory_limit=-1 cannot take the host down:
docker run --memory=512m --memory-swap=512m ... systemd service: MemoryMax=512M in the unit of php-fpm or apache
A WAF may help, but check what it really inspects: truncated bytes, discarded fields, chunked bodies and unlisted multipart parts can all slip through (researcher warning, not run in the lab).
The real fix
Update PHP and restart the workers. Then remove the bandage:
sudo phpdismod -v 7.4 php2shell && sudo rm /etc/php/7.4/mods-available/php2shell.ini # if you used the Debian drop-in
# remove the php_admin lines from the pool and the apache conf you added, remove the size caps you only added for this bug (keep sensible ones)
sudo systemctl restart php7.4-fpm apache2
Then repeat Step 2 and Step 3: the changelog must mention the CVE and info.php must show the values you really want (memory_limit back to a real limit, such as 128M).
Distribution is end-of-life:
Ubuntu LTS: Ubuntu Pro / ESM may carry the fix. Check with: pro security-status (not run in the lab: it needs a subscription token)
Debian: LTS and ELTS releases are tracked in the security tracker, link below
RHEL family: the vendor errata, or move to a supported release
Any: move the application to a supported OS or a supported PHP build (a current container image of an official PHP release, Remi, Sury). Confirm the version is at least 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or that its changelog names the CVE.
Building PHP 7.4/8.0/8.1 from the minimal backports published by the PHP team is possible but is your maintenance burden from then on. Plan the migration instead.
Vendor not yet patched: subscribe to the vendor tracker for CVE-2026-109618, apply the mitigation above meanwhile, and re-check every day.
Common problems
I created the file but nothing changed: the workers were not restarted. Restart php-fpm and the web server, then check through the web server (Step 3).
phpenmod enabled it but another file still wins: look at the numbers in /etc/php/7.4/*/conf.d. Debian reads them in order, the last one wins. Add "; priority=99" to your file and run phpenmod again.
php -i shows the new values and the website does not: php -i reads the CLI file. Check the SAPI the website uses (Step 3).
upload_progress was 0 and is 1 again: a site .user.ini or .htaccess changed it. Lock it with php_admin_value / php_admin_flag.
Uploads over 8 MB stopped working: that is the cap doing its job. Raise it for the one path that needs it.
Everything returns 413: the cap is smaller than your normal requests (a big form, a JSON API). Measure first.
The box gets killed by the out-of-memory killer: memory_limit=-1 plus a heavy request. Set the container or systemd memory ceiling, and consider a high but finite memory_limit once the machine is patched.
Apache says AH00558 about a server name when you restart: harmless warning. Set ServerName.
I cannot find which PHP the site uses: ask the server itself with Step 3, and look at the socket in the nginx config (fastcgi_pass unix:/run/php/php7.4-fpm.sock names the version).
Tips
Patch first when you can. The mitigation list is what you do on the 20 percent of machines where the patch is not possible.
Prioritise by exposure: internet-facing, Apache mod_php, shared hosting, old PHP, then everything else. CISA says to patch internet-facing systems first.
Write the date and the reason in a comment in every file you add. "Why is memory_limit unlimited on this box?" is a bad mystery.
Keep a list of what you changed so that you can undo it after the update. A forgotten memory_limit=-1 is its own problem.
Do not trust a green scan alone. Scanners that check the version string miss backports and flag patched Debian, Ubuntu and Red Hat packages. Use the changelog.
Look at your logs for the period since October 17, not only since disclosure. AWS reported exploitation attempts before PHP published the release.
If anything has been running shells from the web server, the fix is a rebuild from a clean image, new credentials and new keys, not a cleanup.
"My site has no upload form" is not safe: the upload-progress path runs even if the application never uses uploads or sessions.
Sources (checked 2026-11-07)
CISA alert (affected products, KEV date, advice): https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching
PHP security advisory (October 21, 2026): https://www.php.net/archive/2026.php#2026-10-21-5
PHP Foundation blog: https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/
Datadog Security Labs (detection and mitigation): https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/
Qualys (find, mitigate, patch): https://blog.qualys.com/vulnerabilities-threat-research/2026/10/20/php-announces-critical-security-release-for-two-vulnerabilities
Rapid7 and Tenable write-ups: https://www.rapid7.com/blog/post/etr-cve-2026-109618-and-cve-2026-109619-critical-unauthenticated-remote-code-execution-in-php/
https://www.tenable.com/blog/php2shell-cve-2026-109618-cve-2026-109619-frequently-asked-questions-about-critical-php-remote
Plesk (PHP shipped with Plesk): https://support.plesk.com/hc/en-us/articles/44397729587351-Vulnerability-CVE-2026-109618-and-CVE-2026-109619-PHP2Shell-Remote-code-execution-in-PHP-shipped-with-Plesk
Debian security tracker (fixed packages): https://security-tracker.debian.org/CVE-2026-109618
Ubuntu changelog of php8.3 8.3.6-0ubuntu0.24.04.13 and the Rocky 9 php-8.0.30-8.el9_8.1 changelog were read in the lab.