PHP2Shell: How to Patch PHP Properly and Harden It for the Next Critical Flaw
Published: · 15 min read · By Oluniyi D. Ajao
On 21 October 2026 the PHP project shipped security releases for two critical flaws in PHP itself: CVE-2026-109618 and CVE-2026-109619. Both can allow an attacker with no login to run code on a server through crafted web requests. Five days later the US Cybersecurity and Infrastructure Security Agency (CISA) added both to its catalogue of vulnerabilities known to be exploited in attacks. The fixed versions are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.
Security coverage has named the pair "PHP2Shell". This guide explains what the two bugs are in plain terms, then gives developers and sysadmins the exact checks to run: which PHP is really serving your site, whether your distribution has backported the fix, how to make the update take effect, and what to do if you cannot update today. It then goes beyond the patch. PHP2Shell will not be the last critical PHP flaw, and the last part of this guide covers the hardening that limits what an attacker can do when the next one lands before your update does.

What PHP2Shell is
"PHP2Shell" is not an official name. The PHP project, its GitHub security advisories, the CVE records and CISA all use the CVE numbers and plain titles. The nickname comes from the security press, and it describes the outcome: a crafted request against PHP that ends in a shell on the server.
The PHP project's advisory describes the two bugs like this:
"RCE via OOM corruption, CVE-2026-109618: out-of-memory handling can interrupt PHP while internal request data is only partially initialized. Request cleanup may then treat the invalid data as valid."
"RCE via stale global state, CVE-2026-109619: request-owned internal state can survive after its backing memory has been freed. A later request handled by the same worker may use that stale state before normal initialization resets it."
In practice:
- CVE-2026-109618 is reached through PHP's parsing of multipart form uploads. A request built to make PHP run out of memory at the wrong moment leaves internal data half-built, and the cleanup that follows trusts it.
- CVE-2026-109619 is a use-after-free across requests. A PHP worker that serves one request can carry a pointer into memory that has already been released, and a later request on that worker uses it. One of the confirmed paths is session upload progress, which is switched on by default and, in the GitHub advisory's words, needs "no application call to
session_start()".
Both are scored 9.2 out of 10 (critical) on CVSS v4.0 by the PHP security team. The decisive detail is where they sit: in the PHP runtime itself, and on Apache with mod_php they can be triggered before any application code runs. Updating Laravel, Symfony, WordPress or your Composer packages does not fix PHP2Shell. The PHP binary is what has to change.
Who is exposed
The PHP project's advisory is direct: "all PHP installations serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed." It rates the risk highest for Apache with mod_php, "with a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs."
The GitHub advisories add detail per setup. Full remote code execution through the memory bug was validated against mod_php with default memory and input limits. PHP-FPM is judged susceptible by source analysis, and the session path of the second bug was validated against both mod_php and FPM. FrankenPHP is described as likely susceptible to attacks that do not depend on the application; for LiteSpeed's interface, application-specific exploits are described as likely but unvalidated. If your server runs PHP for the web in any of these ways and is not on a fixed release, treat it as vulnerable.
Affected versions, per the advisories: every 8.2 release before 8.2.35, every 8.3 before 8.3.36, every 8.4 before 8.4.27 and every 8.5 before 8.5.12. PHP 8.1 and older get no new releases from the PHP project: "Some distributions and LTS vendors provide backported fixes, so contact your vendor for details, or upgrade to a supported version." The project has also published minimal source backports for 7.4, 8.0 and 8.1, which are patches, not releases. Note too that PHP 8.2 reaches end of life on 31 December 2026, so a server patched to 8.2.35 today needs an upgrade plan anyway.
Is it being exploited?
Yes. CISA added both CVEs to its Known Exploited Vulnerabilities catalogue on 26 October 2026. BleepingComputer reported the same day that honeypots run by Adel Karimi, detection and response lead at Perplexity, recorded confirmed remote code execution against an Apache mod_php server on PHP 8.4.26, one release behind the fix, and that Amazon Threat Intelligence had seen attackers targeting the flaws four days before the patches shipped. Arctic Wolf reported widescale scanning shortly after disclosure.
Proof-of-concept code leaked before the patches. The PHP security team told BleepingComputer that none of it worked, but the confirmed attacks show that working exploits exist. Assume automated scanners are already testing every public PHP endpoint they can find.
For developers: what to check
- Check the PHP that serves the web, not the one in your terminal.
php -vreports the command-line binary. Your site may run a different build through FPM, Apache or a hosting panel. Put a temporary page on the site that printsPHP_VERSIONandPHP_SAPI, load it, then delete it. - Rebuild your containers. If you deploy PHP in Docker, the fix lives in the base image. Rebuild with
docker build --pullon a base that carries a fixed version, redeploy, and confirm the running container reports it. - Your application update does not cover this. Framework and CMS updates are worth doing, but the flaws are in PHP. If you are weighing where to host WordPress, the question that matters here is who patches PHP and restarts it, and how fast.
- On shared or managed hosting, ask your host. Ask which PHP version your site runs and whether the workers were restarted after the update. Both questions matter.
For sysadmins: the patch, step by step
1. Find every PHP that serves web traffic
Servers often carry several PHP builds side by side: distribution packages, a third-party repository, a control panel's own builds, a container. List them all, then find which one each site actually uses. For PHP-FPM, list every master process and the binary behind it:
pgrep -af 'php-fpm: master'
for p in $(pgrep -f 'php-fpm: master'); do sudo readlink /proc/$p/exe; done
If a path ends in "(deleted)", the package was replaced on disk but that process was never restarted, so it is still running the old code.
For Apache with mod_php, the module is part of the Apache process itself, so the installed PHP package is what you check.
2. Read the version string correctly
Distributions often backport security fixes without changing the upstream version number, one reason the distribution you pick matters (see our guide to choosing a VPS operating system). Ubuntu 24.04, for example, fixed both CVEs in package 8.3.6-0ubuntu0.24.04.13: still "8.3.6", but patched. So a version below 8.3.36 on a distribution package does not by itself mean you are vulnerable. Your vendor's security tracker or package changelog settles it:
# Debian / Ubuntu
zgrep -cE 'CVE-2026-10961[89]' /usr/share/doc/php*-fpm/changelog.Debian.gz
# RHEL family distribution packages
rpm -q --changelog php-fpm | grep -cE 'CVE-2026-10961[89]'
A count above zero means the package carries the fixes. Builds that track upstream version numbers, such as third-party repositories, control panel builds and official Docker images, often do not list CVE numbers at all, so for them a zero means nothing: the version itself is the test. Debian's and Ubuntu's public security trackers list the fixed package version per release.
3. Update, then restart every worker
Installing the package does not change running processes. PHP-FPM workers and Apache children keep the old code in memory until they are restarted. After the update, restart the service fully:
systemctl restart php8.3-fpm # or php-fpm, per your distribution
systemctl restart apache2 # or httpd, if you run mod_php
Then confirm it took effect. The service start time must be later than the package install time:
systemctl show php8.3-fpm -p ActiveEnterTimestamp
rpm -q --last php-fpm # RHEL family
zgrep -h " upgrade .*php" /var/log/dpkg.log* | sort | tail # Debian / Ubuntu
4. Deal with end-of-life PHP
Servers on PHP 8.1 or older have three options: a vendor backport (some distributions and long-term support vendors provide one), the PHP project's minimal source patches for 7.4, 8.0 and 8.1 if you build PHP yourself, or an upgrade to a supported branch. Of the three, the upgrade is the only one that also covers the next critical flaw.
5. If you cannot update today
The PHP project is clear that there is no complete workaround: "There are no complete mitigations, updating is the only safe choice." It does list measures that close specific attack paths while you schedule the update. Set them in the FPM pool or Apache configuration, never with ini_set() or .user.ini, and restart every worker afterwards:
- For CVE-2026-109619:
session.upload_progress.enabled=0, which blocks the confirmed session paths. On PHP 8.5, alsofatal_error_backtraces=0. - For CVE-2026-109618:
memory_limit=-1, enforced administratively. This removes PHP's own memory ceiling, so pair it with a container or cgroup memory limit. The GitHub advisory warns againstRLIMIT_ASorRLIMIT_DATA, which trigger the vulnerable path. The PHP project also recommends low request-size limits:post_max_sizearound 8M where you take no uploads, 32M where you do.
In a PHP-FPM pool file:
php_admin_value[session.upload_progress.enabled] = 0
php_admin_value[memory_limit] = -1
In Apache configuration, for mod_php:
php_admin_flag session.upload_progress.enabled off
php_admin_value memory_limit -1
6. Assume an exposed server may have been probed
If an internet-facing server ran a vulnerable PHP from mid-October onwards, patching closes the door but says nothing about who came in before. CISA's catalogue entry points US federal agencies to forensic triage, and that is a sensible bar for everyone else: review web and PHP error logs for unusual multipart POST requests and worker crashes, look for files and processes owned by the web user that you did not create, and rotate credentials the web user could read.
Beyond the patch: harden PHP for the next one
Patching answers this flaw. It does not answer the next one, which may be exploited before any fix exists. The measures below do not stop a PHP flaw from being triggered. They decide how much an attacker gets when one is: a single site's files, or the whole server, its credentials and everything it can reach.
Give every site its own unprivileged PHP user
Run each site in its own PHP-FPM pool, under its own system user, and never as root. Code execution "as the web worker user", in the GitHub advisory's words, is then execution as one site's user, not as the user that can read every site on the server:
[site1]
user = site1
group = site1
listen = /run/php/site1.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10
On the RHEL family use /run/php-fpm/site1.sock and listen.owner = nginx (or apache). Close each site's directory to the others (mode 750), or per-site users can still read each other's files.
Make the code read-only, and never run uploaded files
The PHP user should be able to read the application but not change it. Have a separate deploy user own the code, and give the PHP user write access only to the directories that need it, such as uploads and cache. A worker that cannot write to the code tree cannot rewrite your application. It can still leave files in writable directories, alter the database or stay resident in a running worker, so treat every writable path as untrusted, never include PHP from it, and restart workers after an incident. Then refuse to execute PHP from those writable directories. In nginx, place this above your main PHP location block:
location ~* ^/(?:uploads|wp-content/uploads)/.*\.(?:php\d*|phtml|phar)(?:/|$) {
deny all;
}
For mod_php, put php_admin_flag engine off in a <Directory> block for the upload directory.
Cut what the server exposes
- Remove PHP versions and extensions no site uses. Every installed build is one more to patch.
- Set
expose_php = Offso responses do not advertise your exact PHP version to scanners. - Delete any
phpinfo()pages. - Limit request size in front of PHP, at the web server or proxy (
client_max_body_sizein nginx). The PHP project's own advice for CVE-2026-109618 includes limits on decoded size, fields and parts before requests reach PHP. Field and part counts need a proxy or WAF rule; if you take no form uploads, block multipart requests there.
Restrict what PHP can reach
Much of what an attacker does after getting in needs a connection out: downloading a payload, opening a reverse shell, sending stolen data. Most web workers only ever need to reach a database, a cache and a few known APIs. systemd can enforce that for the PHP-FPM service, and can make system directories read-only to it at the same time. Run systemctl edit php8.3-fpm (or php-fpm) and add:
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
IPAddressDeny=any
IPAddressAllow=localhost 10.0.0.0/8
Adjust IPAddressAllow to what PHP really needs. These rules match IP addresses only: DNS fails unless your resolver is local (127.0.0.53 on Ubuntu) or listed, external APIs need their addresses listed or an outbound proxy, and FPM on TCP behind a remote proxy needs that proxy listed. NoNewPrivileges also stops the setgid helper Postfix uses for mail(), so send mail over SMTP to an allowed relay. Apply this to PHP-FPM only, never to apache2, httpd or nginx. Test on staging, restart, then confirm PHP cannot reach an outside address while the site still works.
Keep secrets small and separate
Assume anything the PHP user can read will be read. Give each site a database account with only the privileges the application needs, keep configuration files with credentials outside the web root and readable only by that site's user, and do not leave keys on a web server that it has no use for, such as backup credentials or cloud provider tokens. A breach then costs one database's data, not your whole estate.
Shorten the time to patch
PHP2Shell went from patch to confirmed attacks in days. Subscribe to the PHP project's announcements and your distribution's security list. Turn on automatic security updates (unattended-upgrades on Debian and Ubuntu, dnf-automatic with apply_updates = yes on the RHEL family). Check that they cover the repository your PHP comes from: on Debian and Ubuntu, third-party PHP repositories are not included by default. Pair them with a check for services still running old code: needrestart on Debian and Ubuntu, dnf needs-restarting -s on the RHEL family. An update that is installed but not yet restarted still leaves the server running the vulnerable code.
Be able to see it, and recover from it
- Ship web, PHP and system logs off the server, so an attacker with a foothold cannot erase them.
- Watch the code tree for changes with a file-integrity tool such as AIDE. On a read-only code tree, any change outside a deploy is a signal.
- Alert when the web user starts processes it normally does not, such as an interactive shell or a download tool. Baseline first: PHP's
mail()startsshto call sendmail. - Keep backups somewhere the web server cannot write to or delete, and test a restore. When a server cannot be trusted, rebuilding from a clean image and a known-good backup is faster than cleaning it.
Do not confuse it with CVE-2026-115560
On 23 October the PHP project published a separate informational advisory for a less likely session upload progress issue, CVE-2026-115560, which is not part of PHP2Shell and was not fixed in the 21 October releases. Its advice is the same setting: "We suggest you set session.upload_progress.enabled=0 until the next regular bugfix release, to fully protect from an unlikely RCE vector."
The checklist
For PHP2Shell, now:
- List every PHP build on the server and which one serves each site.
- Confirm each is on 8.5.12, 8.4.27, 8.3.36, 8.2.35 or a vendor build carrying the fixes.
- Restart every FPM pool and Apache instance, and check the start time against the install time.
- Rebuild and redeploy containers from patched base images.
- Plan off PHP 8.1 and older now, and off 8.2 before 31 December 2026.
- Set
session.upload_progress.enabled=0if you do not use upload progress. - Review logs on any server that was exposed from mid-October onwards.
For the next one:
- One PHP-FPM pool and one unprivileged user per site, never root.
- Code owned by a deploy user, read-only to PHP; no PHP execution in upload directories.
- Unused PHP builds and extensions removed,
expose_php = Off, request size limited in front of PHP. - Outbound connections from PHP limited to what the application needs.
- Least-privilege database accounts, and no stray keys on the web server.
- Automatic security updates plus a restart check.
- Logs off the server, file-integrity alerts, and tested backups the web server cannot touch.
On a self-managed server, every one of these steps is yours to run. AFRICLOUD Cloud Servers come with full root access, so the PHP version, its update and its restart are under your control.
Sources
- PHP project: security advisory and release announcements, 20 to 23 October 2026
- PHP security advisories on GitHub: GHSA-w8hw-w9fx-j79w (CVE-2026-109618) and GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
- NVD: CVE-2026-109618 and CVE-2026-109619
- CISA: two known exploited vulnerabilities added, 26 October 2026
- PHP manual: session runtime configuration
- Ubuntu: CVE-2026-109618; Debian: security tracker
- BleepingComputer: PHP2Shell flaws exploited in the wild; Arctic Wolf: PHP2Shell analysis