php2shell (CVE-2026-109618/109619): patching PHP and mitigating until you can
Two PHP flaws rated 9.2, exploitable without authentication before any application code runs: fixed versions, temporary mitigations, checks and a test lab.
Key takeaways
- Two CVEs rated 9.2, fixed in 8.2.35, 8.3.36, 8.4.27 and 8.5.12; PHP 8.1 and older get no official fix.
- Exploitable without authentication, before any application code runs; mod_php is the most exposed setup.
- Immediate mitigation:
session.upload_progress.enabled = Offinphp.inior the pool (not.user.ini), then replace every worker. - Check every binary (FPM, mod_php, CGI, panel): the terminal's
phpsays nothing about PHP-FPM. - The github.com/ikow/php2shell-poc lab is for testing your mitigation on an isolated machine, never on someone else's system.
On October 20, 2026, the PHP security team announced an emergency release for two vulnerabilities rated 9.2 in CVSS v4.0, scheduled for the next day at 14:00 UTC, with no technical details. Details leaked early, a tool called "PHP2Shell" circulated, and the fixed versions shipped on October 21: 8.2.35, 8.3.36, 8.4.27 and 8.5.12.
The best technical read so far is Qi Deng's analysis, "php2shell: Dissecting PHP's Critical Session Upload Progress Vulnerabilities", which comes with a reproducible lab on GitHub: github.com/ikow/php2shell-poc. This article covers the defensive side: what is affected, what to update, what to switch off in the meantime, and how to check that the mitigation is actually in effect.
What the official advisory says
The php.net advisory describes two engine flaws:
- CVE-2026-109618: code execution through out-of-memory corruption. An allocation failure can interrupt PHP while internal request data is only partially initialized; request cleanup then treats that data as valid.
- CVE-2026-109619: code execution through stale global state. Request-owned internal state can outlive the memory backing it; a later request handled by the same worker may use it before it is reset.
The scope is wide: every PHP installation that serves web requests, whether through PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP or LiteSpeed. The advisory rates the risk highest for mod_php, with a demonstrated exploit in which an unauthenticated request triggers the attack before any application code runs.
The attack surface: session.upload_progress
Public research converges on a feature few administrators know about: session.upload_progress. Introduced in PHP 5.4 and enabled by default, it records the progress of multipart/form-data file uploads in the session, to feed a progress bar.
The danger lies in where it runs: in the SAPI layer, while the request body is being parsed, before the application takes over. In practice:
- no authentication is required;
- no application code is involved;
- any PHP entry point that accepts a POST will do, even one never meant to receive a file;
- the feature writes data partly chosen by the sender into session files.
What Qi Deng's research uncovered
Auditing the source code, the researcher identifies several flaws in upload progress handling:
- Session fixation: the session ID can be read from the request body without honouring
session.use_only_cookies(fix proposed in PR #22113). - Unfiltered filename: the uploaded file's name is stored as-is in the session data.
- Use-after-free in unserialization: an object destructor called during cleanup can re-enter
unserialize()and read memory that has already been freed, as AddressSanitizer confirms. - Session serializer not reset between requests under FPM.
- Destructors run even when session decoding fails.
From these he derives three chains leading to code execution. Their prerequisites are what matter when assessing your exposure:
| Chain | What the application must have |
|---|---|
| Session file injection + local file inclusion | A local file inclusion (LFI) flaw, the classic case of poorly written WordPress plugins |
| Unserialization use-after-free | A call to unserialize() on reachable data (Laravel and Magento are cited) |
| Serializer mismatch between directories | Two directories with different session.serialize_handler values (via .user.ini, common when several applications share a server) and an exploitable class already loaded, typically through opcache.preload |
All three chains were verified in a lab on PHP 8.2.34. One caveat: these are the researcher's descriptions, written before the official details were published. They partly match the advisory: the serializer that survives from one request to the next is reminiscent of the "stale global state" in CVE-2026-109619. The advisory, however, also describes an out-of-memory path the research does not cover. Everyone agrees on two points: update, and disable session.upload_progress in the meantime.
Am I affected?
According to the research, every version from PHP 5.4.0 up to 8.5.11 is affected. The fixed versions are 8.2.35, 8.3.36, 8.4.27 and 8.5.12. Branches 8.1 and older no longer receive fixes from the PHP project.
The classic trap is checking the wrong PHP. The php command in your terminal is the CLI binary: it tells you nothing about PHP-FPM, a mod_php loaded into Apache, or the versions a hosting panel ships. You have to query every binary that actually serves web traffic:
# Which PHP binaries actually serve requests?
ps -eo args | grep -E 'php-fpm|php-cgi|lsphp|frankenphp' | grep -v grep | sort -u
apache2ctl -M 2>/dev/null | grep -i php # mod_php loaded?
# Effective version and settings, binary by binary
php-fpm8.3 -v
php-fpm8.3 -i | grep -E 'session.upload_progress.enabled|session.use_only_cookies'
# Fixed packages available (Debian/Ubuntu)
apt list --upgradable 2>/dev/null | grep -i php
Two blind spots: -i does not show overrides set per FPM pool (php_admin_value) or per virtual host, which you have to read in their own files; and on Ubuntu the upstream number does not change, since the fix ships as a new package revision. A "8.3.6" PHP may or may not be fixed: only your distribution's security notice can tell. Debian 13, for its part, received 8.4.27 directly.
Updating: the only real fix
- Distribution packages: install the fixed revision as soon as it is published, then restart PHP-FPM, and Apache if mod_php is loaded. As long as old workers run, the old code stays in memory.
- Hosting panels (Plesk, cPanel…): they ship their own builds, one per installed branch. Update all of them, including those no site uses anymore.
- Containers: rebuild images from a fixed base; restarting a container does not change the PHP inside it.
- PHP 8.1 and older: the security team published minimal source backports for 7.4, 8.0 and 8.1. Otherwise, use an LTS vendor or migrate. If you rely on the volunteer-run remirepo repository, the advisory warns it would have no update on release day and recommends distribution packages or building from source.
- PHP 8.2 reaches end of life on December 31, 2026: now is the time to plan what comes next.
Until the package lands: temporary mitigations
The advisory gives one mitigation per CVE. The one for CVE-2026-109619 is the simplest and most useful:
; /etc/php/8.3/fpm/conf.d/99-php2shell.ini
; (same file in cgi/conf.d and apache2/conf.d if those SAPIs are in use)
session.upload_progress.enabled = Off
session.use_only_cookies = On
- The setting must apply before the request is parsed: in
php.ini, an FPM pool or the server configuration. Neitherini_set()nor a.user.iniwill do, because they come too late. - Every worker must be replaced: reload or restart each PHP-FPM service, restart Apache for mod_php.
- On PHP 8.5, add
fatal_error_backtraces = 0, which closes another confirmed path. - The research also recommends the same
session.serialize_handlerin every directory on a given server.
The mitigation for CVE-2026-109618 is trickier: memory_limit = -1 enforced through php_admin_value (so the application cannot override it), a low post_max_size (8M without file uploads, 32M otherwise, with upload_max_filesize below it), size and part-count limits at the web server, and a cgroup or container memory limit as a safety net. The advisory says not to use RLIMIT_AS or RLIMIT_DATA, which can trigger the very allocation failure that is vulnerable.
Then check instead of assuming:
# Does any application use upload progress?
grep -rn --include='*.php' -E 'upload_progress|PHP_SESSION_UPLOAD_PROGRESS' /var/www
# Valid configuration, reload, effective value
sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm
php-fpm8.3 -i | grep session.upload_progress.enabled # expected: Off => Off
Field notes: what gets forgotten when hardening a fleet
I applied this mitigation the same day to around fifteen PHP binaries spread across several servers. The setting itself is one line. The real work was knowing where to put it:
- The forgotten mod_php. One Apache still loaded mod_php while the sites ran through PHP-FPM. It does not show up as "php" in the process list, yet it is the setup the advisory rates as most exposed.
- Ghost panel versions. A hosting panel installs several branches side by side, including end-of-life ones no site uses anymore. Each has its own configuration directory, and each needs the mitigation.
- Forgotten machines. A VM on PHP 8.1, with no fix available, started automatically with its host and had been running for several days. It was shut down rather than hardened: the best mitigation is still not exposing what you do not use.
- Before/after testing. For each server, a list of representative URLs, with the HTTP status and response size recorded before and after. That test showed that a related hardening step,
cgi.fix_pathinfo=0, broke WordPress's/index.php/wp-json/routes under Plesk: it was rolled back. Disablingsession.upload_progressdid not change a single response. - The exit note. Every mitigation file is logged, together with the decision to make after the update: remove it or keep it. For
session.upload_progress, no application uses it, so keeping it costs nothing.
Spotting an attempt
A few signals worth watching, with no illusions: their absence proves nothing.
multipart/form-dataPOSTs to URLs that never receive files.- The field named by
session.upload_progress.name(PHP_SESSION_UPLOAD_PROGRESSby default) in requests to applications that do not use it. If your WAF inspects request bodies, it is an easy rule to add, and as partial as the other mitigations. - PHP-FPM workers dying on a signal:
grep -E 'exited on signal (6|11)' /var/log/php8.3-fpm.log. - Files with unexpected content in the
session.save_pathdirectory. - Processes started by the web server user (a shell,
curl,wget), which eBPF observability with Falco catches as they start.
The lab: test your mitigation, not other people's production
The ikow/php2shell-poc repository provides a Docker environment (a deliberately vulnerable PHP 8.2.34, FPM behind nginx), three verification scripts (one per chain, with no offensive payload according to its author) and a detailed technical analysis in docs/ANALYSIS.md.
For an administrator, its value is in replaying the scenario on an isolated machine, applying the mitigation, then seeing what it covers and what it does not. Both the researcher and the official advisory say so: turning off upload progress does not close every path. The lab lets you see it for yourself, and it is the best proof that only the update fixes everything.
The lesson
A feature enabled by default since 2012, used by almost nobody, provided the entire attack surface. It is the most concrete argument there is for disabling what you do not use, before a CVE makes the point for you. The second lesson is less visible: on the day of a critical flaw, applying the fix takes ten minutes, while knowing which PHP binaries run, and where, can take the whole day. That inventory is best built in calm times, like the rest of a Linux server security checklist.
The pattern is not new: a framework layer reachable before any authentication, as with React2Shell on the Next.js side. The more code runs before your application, the more it counts toward your attack surface.
Sources
- php.net security advisory, October 21, 2026 (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp)
- Qi Deng, "php2shell: Dissecting PHP's Critical Session Upload Progress Vulnerabilities"
- Lab and verification scripts: github.com/ikow/php2shell-poc
Frequently asked questions
Is disabling session.upload_progress enough?
Does my application use PHP's upload progress?
grep -rn -E 'upload_progress|PHP_SESSION_UPLOAD_PROGRESS' over your sources settles it: with no match, disabling it has no visible effect.My distribution still shows PHP 8.3.6: am I fixed?
I am still on PHP 8.1 or older: what should I do?
Can I run the lab scripts against my production?
Did you enjoy this article?
Was this article helpful?
Thanks for your feedback!
Comments
Cybersecurity and Linux administration expert. I help companies secure and optimize their critical infrastructures.