Security incident · Published 2026-10-23
PHP2Shell: What Happened and What to Do Right Now
A practical response guide to the PHP2Shell vulnerabilities: identify affected runtimes, apply the vendor fix, verify worker restarts, and treat interim mitigations as incomplete.
TL;DR
- Two critical, unauthenticated remote code execution bugs in PHP itself, CVE-2026-109618 and CVE-2026-109619 (nicknamed PHP2Shell), were fixed on 21 October 2026 in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.
- The PHP project states there is no complete mitigation. Updating is the only safe choice.
- The flaw is in the PHP runtime, not in your application. Updating your framework or CMS does not help.
- On Debian and Ubuntu,
php -vdoes not tell you whether you are patched, because distributions backport the fix without changing the version number. Check the package changelog. - Installing the package is not enough. Restart the PHP workers, and rebuild and redeploy containers.
- Reports about active exploitation contradict each other. Treat the bugs as exploitable.
Timeline
- 20 October: A Hacker News post (since deleted) claimed two critical PHP RCE flaws would be disclosed on 22 October and named both CVE IDs. watchTowr then shared the same identifiers and said it had reproduced CVE-2026-109618. PHP announced an emergency security release for the next day.
- 21 October, 14:00 UTC: PHP released 8.5.12, 8.4.27, 8.3.36 and 8.2.35 together with a security advisory.
- 21-22 October: Vendor advisories, proof-of-concept claims, a Cloudflare WAF rollout, scanners, and a wave of precautionary service shutdowns followed.
PHP scored both bugs 9.2 on CVSS v4.0. The "CVSS 10.0" figure that spread on social media came from the leak and early researcher statements, not from PHP's advisory.
The two vulnerabilities
| CVE | Class | Description |
|---|---|---|
| CVE-2026-109618 | RCE via OOM corruption | Out-of-memory handling can interrupt PHP while internal request data is only partly initialized. Request cleanup may then treat that half-built data as valid. |
| CVE-2026-109619 | RCE via stale global state | Request-owned internal state can survive after its backing memory has been freed. The next request on the same worker may use that stale state before it is reset. |
The advisory lists PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed as affected, and rates mod_php as the highest risk. Initial reporting indicates the issue is triggered during multipart/form-data parsing, which happens before your application code runs. That is why blocking multipart requests at the edge is among PHP's own interim mitigations.
Why it became a drama
1. The leak forced an early release. Disclosure was planned for 22 October. An unverified rumor turned out to be accurate, and patches landed unevenly across distributions and repositories.
2. The "just disable upload progress" advice fell apart. The early advice was to set session.upload_progress.enabled=0. That setting blocks only the confirmed session paths of CVE-2026-109619 and does nothing for CVE-2026-109618. GMO Flatt Security, who first suggested it, later revised its write-up to say the attack remains possible. A PHP core developer said publicly that patching or upgrading PHP is the only mitigation. If disabling upload progress was all you did, you are still exposed.
3. Exploitation reports contradict each other.
- watchTowr reported exploitation in the wild through its honeypot network, starting 21 October.
- Sophos first reported successful exploitation in customer environments, then corrected that: its telemetry shows scanning and a small number of failed attempts, with no evidence of a successful compromise.
- As of the CISA KEV file released 21 October at 14:04 UTC, neither CVE was listed.
- VulnCheck said it had a weaponized exploit for CVE-2026-109618 by early 22 October, available only to its paying customers.
- Tanium noted that no public indicators of compromise had been released as of 21 October.
Not being in KEV does not mean you are safe. Patch as if exploitation is happening.
4. Packages did not arrive everywhere at once. Remi's repository did not publish updates on release day, and PHP's advisory urged Remi users to switch to distribution packages or build from source. Other states reported on 21-22 October:
- Ubuntu shipped fixes in USN-8948-1, and Ondřej Surý's PPA shipped fixed builds.
- Debian had fixed packages for 12 and 13 according to Tanium (as of 21 October, 16:30 UTC).
- RHEL packages were still pending as of 21 October.
- Official Docker
phpimages lagged at first (reported as of early 22 October, Japan time).
Check each channel again today. These statuses were accurate when reported and may have changed.
5. Precautionary outages. Many organizations took services offline while they waited for the fix, including BambooHR, addy.io, the GrapheneOS forum and a long list of Japanese companies. Some of those organizations had already turned off session upload progress and blocked multipart requests, and took their services down anyway.
Response playbook
1. Inventory every PHP runtime
Include VMs, containers, shared and managed hosting, and any forgotten internal tool that serves HTTP. For managed platforms where you do not control the PHP binary, confirm the provider's patch status directly with them.
For container workloads on Kubernetes:
# Inventory PHP images in the selected Kubernetes cluster; review results locally.
kubectl get pods -A -o json | jq -r '
.items[] | .metadata.namespace as $ns | .metadata.name as $p
| .spec.containers[] | select(.image | test("php"; "i"))
| "\($ns)/\($p)\t\(.image)"'2. Patch through the channel PHP was installed from
Debian and Ubuntu (replace 8.3 with your branch):
# Refresh APT metadata, then review the proposed package transaction.
sudo apt update
sudo apt install --only-upgrade 'php8.3-*'RHEL, AlmaLinux and Rocky Linux:
dnf module list php # know which stream is enabled
sudo dnf upgrade 'php*'Other channels:
- Built from source or Windows: use the fixed releases from php.net.
- Remi: no update was published on release day. Use distribution packages or build from source, as PHP's advisory suggests.
- Ondřej Surý's repository: fixed builds were published. They do not list the CVE IDs in the changelog, so confirm by build date (builds dated 20261021) rather than by grepping the changelog.
- Docker: pulling a new image does not change running containers. Rebuild your own images on a patched base and recreate the containers:
# Rebuild the project image from its configured base and recreate its containers.
docker compose build --pull --no-cache
docker compose up -d --force-recreate3. Verify the fix, not just the version number
Distribution packages usually backport only the fix, so the version string often stays the same. Check the package changelog for the CVE IDs instead.
Debian and Ubuntu:
# Inspect package changelog entries; verify the package name on this distribution.
zgrep -E "CVE-2026-10961[89]" /usr/share/doc/php*-common/changelog.Debian.gzRHEL family:
# Inspect the installed package changelog; confirm its package name with the vendor.
rpm -q --changelog php-common | grep -E "CVE-2026-10961[89]"If nothing is printed, the installed package does not contain the fix.
4. Restart the workers
Updating a package does not replace code already loaded into running processes.
# Restart only the PHP service that is installed and used by this host.
sudo systemctl restart php8.3-fpm # php-fpm on RHEL family
sudo systemctl restart apache2 # httpd on RHEL family, if you use mod_phpTo find workers still running the pre-update binary:
# Find workers still mapped to a deleted executable after a package replacement.
for pid in $(pgrep -f 'php-fpm|apache2|httpd'); do
if ls -l "/proc/$pid/exe" 2>/dev/null | grep -q '(deleted)' \
|| grep -q 'libphp.*(deleted)' "/proc/$pid/maps" 2>/dev/null; then
echo "STALE: pid $pid ($(cat /proc/$pid/comm))"
fi
doneNote that php -v on the command line reports the CLI binary, not the web server's PHP. If you confirm a version through phpinfo() in a browser, delete the file afterwards.
5. End-of-life branches (8.1 and older)
PHP released official fixes only for 8.2 through 8.5. For older branches you have these options:
| Option | Pros | Cons |
|---|---|---|
| Upgrade to a supported branch | The real fix | Application compatibility work |
| Vendor or distribution backport | Fastest, no code change | Tied to the vendor, and the branch stays EOL |
| PHP's minimal source patches for 7.4, 8.0 and 8.1 | Free, from the PHP project itself | You build and maintain it yourself. There are no tarballs or Windows binaries |
Details that were reported:
- The PHP security team published one minimal patch branch each for 7.4, 8.0 and 8.1, named
PHP-<version>-CVE-2026-109618-109619in the php/php-src repository. The advisory calls them minimal source backports. - Ubuntu's USN-8948-1 also covers PHP 8.1 on Ubuntu 22.04. USN-8948-2 covers older Ubuntu releases, and for 20.04 and 18.04 it requires Ubuntu Pro.
- TuxCare (paid), Ondřej Surý's repository and CloudLinux have shipped fixed packages for branches PHP no longer updates.
- PHP 8.2 reaches end of life on 31 December 2026. Plan the move to a newer branch now.
6. Interim mitigations, only if you cannot patch within hours
PHP states that none of these make an installation safe. Each blocks only specific attack paths.
| Target | Setting | Caveat from PHP |
|---|---|---|
| CVE-2026-109618 | Lower post_max_size (8M if you do not need uploads, 32M if you do) and keep upload_max_filesize below it | None listed |
| CVE-2026-109618 | Limit request size, input field count, multipart part count, header count and nesting depth at the web server or proxy | Limits must also apply to requests without a usable Content-Length |
| CVE-2026-109618 | memory_limit=-1, set so applications cannot override it (for example php_admin_value in the FPM pool) | An underlying allocation failure can still reach the vulnerable code. PHP recommends a separate container or cgroup memory limit with this |
| CVE-2026-109619 | session.upload_progress.enabled=0, then restart every worker | Set in php.ini, the FPM pool or server config. Do not rely on ini_set() or FPM .user.ini. Blocks only the confirmed session paths |
| CVE-2026-109619 | On PHP 8.5: fatal_error_backtraces=0, then restart every worker | Blocks only the confirmed fatal backtrace path |
Example FPM pool settings (a pool file such as /etc/php/8.3/fpm/pool.d/www.conf):
; Example pool limits only; confirm values and supported syntax with the PHP vendor.
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M
php_admin_value[session.upload_progress.enabled] = 0Block multipart POSTs at the edge. The advisory for CVE-2026-109618 says that if your application does not use multipart/form-data, such requests should be blocked before they reach PHP. Caveats:
- It breaks every file upload and any form that happens to submit as multipart, including some checkout forms.
- It does not close everything. CVE-2026-109619 also has a path through warnings from settings applied via
.htaccessor server config, which has nothing to do with multipart. - In one published AWS WAF test, rate-based rules still let 23 to 30 requests through before blocking, even with the limit set to 10. A simple match-and-block rule blocked from the first request.
An nginx sketch (adapt to your layout and test on staging first):
# http {} context
map "$request_method:$content_type" $block_multipart {
default 0;
"~*^POST:\s*multipart/form-data" 1;
}
# server {} context
client_max_body_size 8m;
location / {
if ($block_multipart) { return 403; }
try_files $uri /index.php?$query_string;
}An Apache sketch:
# Example rewrite filter; test in staging and confirm the active Apache modules.
RewriteEngine On
RewriteCond %{REQUEST_METHOD} =POST
RewriteCond %{HTTP:Content-Type} multipart/form-data [NC]
RewriteRule ^ - [F]
LimitRequestBody 8388608Cloudflare. Cloudflare deployed WAF rules for the known exploit vectors to WAF users on all plans, from 03:02 UTC on 21 October. If your origin also accepts connections that do not come through Cloudflare, an attacker who knows the origin IP can send requests directly and skip those rules. Restrict the origin to Cloudflare.
Scanning. Searchlight Cyber released an open-source scanner, php2shell-scanner, that tests whether a host is exposed. Run it only against systems you own.
7. Look for signs of compromise
Public indicators of compromise were not available as of 21 October, so any check is behavioral, and finding nothing proves nothing. The window to examine starts at the leak on 20 October.
# New or modified PHP files since the leak
find /var/www -type f \( -name '*.php' -o -name '*.phtml' -o -name '.htaccess' \) \
-newermt '2026-10-20' -printf '%TY-%Tm-%Td %TH:%TM %u %p\n' | sort
# PHP-FPM worker crashes in the exposure window
sudo journalctl -u 'php*-fpm' --since "2026-10-20" | grep -i 'exited on signal'If you use auditd, a rule on execve by the web server user will show whether PHP workers spawn shells or download tools. Replace www-data with your web server user (for example apache or nginx on RHEL family).
# Record execution events only on systems where this approved audit rule is appropriate.
sudo auditctl -a always,exit -F arch=b64 -S execve -F euid=$(id -u www-data) -k php_exec
sudo ausearch -k php_exec -iIf you find evidence of compromise, patching is no longer the fix. Isolate the host, preserve disk and memory for analysis, rotate every secret the PHP user could read, and rebuild from a clean image instead of cleaning in place.
8. Prevent a repeat
Add a version gate to your container build. This works for images that use upstream version numbers (official images and source builds). It does not work for distribution packages with backported fixes, where the version string stays old.
#!/usr/bin/env bash
# php-min-version-gate.sh - fail the build if upstream PHP is older than the fixed release
set -euo pipefail
ver=$(php -r 'echo PHP_VERSION;')
case "$ver" in
8.5.*) min=8.5.12 ;;
8.4.*) min=8.4.27 ;;
8.3.*) min=8.3.36 ;;
8.2.*) min=8.2.35 ;;
*) echo "PHP $ver has no upstream fix: EOL branch, use a vendor backport or upgrade"; exit 1 ;;
esac
if [ "$(printf '%s\n%s\n' "$min" "$ver" | sort -V | head -n1)" != "$min" ]; then
echo "PHP $ver is older than $min (PHP2Shell), failing the build"; exit 1
fi
echo "PHP $ver OK"Beyond that, make sure your inventory covers the PHP runtime itself and not only Composer dependencies, and that you can rebuild and redeploy every image within hours.
What this article does not claim
- The real extent of exploitation. The public reports above are contradictory, and details may have changed since writing.
- The current patch status of any specific distribution, repository, Docker tag or managed platform. Check the vendor's own page.
- That any interim mitigation is sufficient. According to PHP, only the update is a complete fix.
Sources
- PHP security advisory — CVE-2026-109618: https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
- PHP security advisory — CVE-2026-109619: https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp
- PHP 8.2.35 release notes: https://github.com/php/php-src/releases/tag/php-8.2.35
- Help Net Security: https://www.helpnetsecurity.com/2026/10/21/php2shell-php-vulnerabilities-patched-cve-2026-109618-cve-2026-109619/
- BleepingComputer: https://www.bleepingcomputer.com/news/security/php-warns-admins-of-two-critical-flaws-emergency-patches-coming-tomorrow/
- watchTowr FAQ: https://watchtowr.com/intelligence/php-cve-2026-109618-faq/
- Sophos: https://www.sophos.com/en-us/blog/proof-of-concept-exploit-released-for-php-vulnerabilities-109618-109619
- Brandefense: https://brandefense.io/blog/php2shell-php-rce-cve-2026-109618-109619/
- Tanium: https://www.tanium.com/blog/php2shell-responding-to-cve-2026-109618-and-cve-2026-109619
- GuidePoint Security: https://www.guidepointsecurity.com/blog/how-to-patch-php2shell/
- lilting channel (PHP advisory mitigations, distribution status, EOL backports): https://lilting.ch/en/articles/php2shell-official-patch
- Konfirmity advisory summary: https://www.konfirmity.com/advisories/php2shell-php-remote-code-execution
This article is educational information, not incident-response or legal advice. Verify package status with your operating system, hosting, or runtime vendor.