Home

php2shell (CVE-2026-109618/109619): patching PHP and mitigating until you can

Sécurité 10 min read

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 = Off in php.ini or the pool (not .user.ini), then replace every worker.
  • Check every binary (FPM, mod_php, CGI, panel): the terminal's php says 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 sentence to remember from the advisory: there is no complete mitigation, updating is the only safe choice. Temporary settings reduce exposure; they do not make an installation safe.

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:

  1. Session fixation: the session ID can be read from the request body without honouring session.use_only_cookies (fix proposed in PR #22113).
  2. Unfiltered filename: the uploaded file's name is stored as-is in the session data.
  3. 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.
  4. Session serializer not reset between requests under FPM.
  5. 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:

ChainWhat the application must have
Session file injection + local file inclusionA local file inclusion (LFI) flaw, the classic case of poorly written WordPress plugins
Unserialization use-after-freeA call to unserialize() on reachable data (Laravel and Magento are cited)
Serializer mismatch between directoriesTwo 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. Neither ini_set() nor a .user.ini will 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_handler in 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.

My call: the first mitigation takes minutes and has no side effects when no application uses upload progress. The second should not be improvised in production: removing PHP's memory limit without a cgroup limit behind it trades a vulnerability for a risk of exhausting the server's RAM. I have not applied it at this stage.

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. Disabling session.upload_progress did 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-data POSTs to URLs that never receive files.
  • The field named by session.upload_progress.name (PHP_SESSION_UPLOAD_PROGRESS by 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_path directory.
  • 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.

Precautions: a disposable VM, no lab port exposed (it is vulnerable by design), and never point the scripts at a system that is not yours or that you lack written authorization to test.

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

Frequently asked questions

Is disabling session.upload_progress enough?
No. The official advisory states this setting blocks the confirmed session-based paths, not other variants, and that no mitigation makes an installation safe. It reduces exposure while you install 8.2.35, 8.3.36, 8.4.27 or 8.5.12, or your distribution's fixed revision.
Does my application use PHP's upload progress?
Rarely. Modern progress bars rely on browser-side progress events, not on this feature. A 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?
The upstream number is not enough. Ubuntu backports fixes into a new package revision without changing the displayed version. Rely on your distribution's security notice and the installed revision, then check that PHP services were actually restarted.
I am still on PHP 8.1 or older: what should I do?
The PHP project no longer releases versions for these branches. The security team made minimal source backports available for 7.4, 8.0 and 8.1; otherwise, use an LTS vendor or migrate. In the meantime, apply the temporary mitigations and shut down whatever does not need to run.
Can I run the lab scripts against my production?
Better not. The lab is built for an isolated Docker environment, and its scripts write session data on the target to exercise code whose flaws involve memory corruption. Reproduce your configuration in a disposable VM, apply the mitigation and observe the result. Never target a system without written authorization.

Did you enjoy this article?

Was this article helpful?

Thanks for your feedback!

Comments

MR
Morgann Riu

Cybersecurity and Linux administration expert. I help companies secure and optimize their critical infrastructures.

CVE PHP RCE PHP-FPM session.upload_progress security hardening

Recommended for you

Back to the blog
↑