All articles
#php#cve-2026-109618#cve-2026-109619#rce#patch-management#php-fpm#linux-packages#incident-response

PHP2Shell: two critical PHP vulnerabilities, and why "patched" needs checking

Alexander Norman

CVE-2026-109618 and CVE-2026-109619 allow unauthenticated remote code execution in PHP itself, before any application code runs, and attacks were reported before the fixed versions were out. PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix them. This post covers what is affected, why the official stopgaps are not protection, and the practical traps we saw on release day: packages dated today that do not contain the fix, CDNs that can be walked around, and why nobody can scan their way to certainty.

The short version

On 21 October 2026 the PHP project released PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 to fix two critical vulnerabilities. Both are rated CVSS v4 9.2 and both can allow unauthenticated remote code execution. Sansec, which has named the pair PHP2Shell, states that the bugs affect every web server running PHP 5.4 or later.

This is not a WordPress plugin or a framework bug. The flaw is in PHP, and according to the PHP advisory an unauthenticated request can trigger it before any application code runs. Your own code, your login page and your application firewall rules inside the app do not get a say.

Attacks did not wait for the release. Cloudflare reported early attacks against PHP sites at 07:43 UTC on 21 October, before the fixed versions were published.

Update PHP on every server that serves web requests. There is no complete workaround.

What the vulnerabilities are

The PHP advisory describes them in one sentence each:

  • CVE-2026-109618, RCE via OOM corruption: out-of-memory handling can interrupt PHP while internal request data is only partially initialized, and request cleanup may then treat the invalid data as valid.
  • CVE-2026-109619, RCE via stale global state: request-owned internal state can survive after its memory has been freed.

According to php.net, all PHP installations serving web requests are at risk, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. The PHP project considers the risk highest for mod_php.

Which versions are fixed

Branch Fixed version Status
8.5 8.5.12 Supported
8.4 8.4.27 Supported
8.3 8.3.36 Supported
8.2 8.2.35 Supported until 31 December 2026
8.1 and older No official release Vendor backport or upgrade

PHP 8.1 and older no longer receive releases from the PHP project. Given the severity, the PHP security team did publish minimal source backports for 7.4, 8.0 and 8.1 for people who build from source, but if you run packaged PHP on these branches you depend on your distribution or LTS vendor, or on upgrading.

The stopgaps are not protection

The advisory lists measures for people who cannot update immediately, and is explicit that they "reduce exposure to specific attack paths but do not make an installation safe". The release notes put it more bluntly: "There are no complete mitigations, updating is the only safe choice."

The most discussed one is session.upload_progress.enabled=0. According to php.net it blocks the confirmed session paths of CVE-2026-109619, not other variants, and it does nothing for CVE-2026-109618. It also has to be set where it applies before PHP parses the request: in php.ini, the FPM pool or the server configuration, followed by a restart of every worker. Setting it with ini_set() or in an FPM .user.ini does not work. On PHP-FPM, remember that the CLI php.ini is not the one your web requests use.

The advisory lists further per-CVE measures. For CVE-2026-109618 it recommends memory_limit=-1, set where applications cannot override it (for example php_admin_value in the FPM pool), and a low post_max_size: 8M if no file uploads are needed, 32M otherwise, with upload_max_filesize kept below it. It explicitly warns against substituting RLIMIT_AS or RLIMIT_DATA. Sansec notes that memory_limit = -1 lets a single request consume all server memory, so it belongs in the hours before patching, not in the permanent configuration. Treat all of these as buying time, never as an alternative to the update.

Lessons from release day

1. A package dated today is not necessarily a fixed package. Distributions and LTS vendors received the patches in advance, but that does not mean every build you see today contains them. Verify that the CVE numbers are in the package changelog before you consider a server done:

bash
# Debian/Ubuntu
zcat /usr/share/doc/php8.3-common/changelog.Debian.gz | grep -E 'CVE-2026-10961[89]'

# RHEL/Alma/Rocky and other RPM-based systems
rpm -q --changelog php-common | grep -E 'CVE-2026-10961[89]'

As an example, on 21 October the Sury repository published a php8.1 build dated the same day whose changelog listed only earlier backported CVEs, not 109618 or 109619. That may well change, but it shows why a fresh date is not proof. Remi's repository, widely used on Enterprise Linux, was expected by Sansec not to publish on 21 October, but shipped fixed packages later the same day, including a backport for 8.1. There the changelog does name both CVEs, which is exactly the difference to look for.

2. session.upload_progress is a partial stopgap. See above. If it is the only thing you changed today, you are still exposed.

3. A CDN or WAF in front only helps if the origin cannot be reached around it. Cloudflare deployed WAF rules for all customers, and its CTO called them a "stopgap". Even as a stopgap, they only apply to traffic that actually passes through the CDN. If the origin server answers anyone on the internet, an attacker simply goes straight to it. Restrict the origin to CDN traffic, and be aware that an allowlist of CDN IP ranges is weaker than it looks; we wrote about that in why Cloudflare proxy alone is not enough.

4. You cannot scan your way to certainty. Most servers do not reveal their PHP version, so external scanning cannot reliably tell who is vulnerable. And an active check that would actually prove the vulnerability is, in practice, the attack itself. Treat every PHP server as affected until you have verified the patched version on the server itself. A scan with no findings is not evidence of safety.

5. Locked to an old branch? Check the vendor first. Applications held on an old PHP branch by encoded or third-party modules are the hardest case. Check whether the vendor has a newer release or reinstall of the module that raises the supported PHP version. Sometimes that is all that stands between you and a supported branch.

6. On Enterprise Linux, move carefully. AppStream and third-party repositories may lag behind the upstream release. If encoded loaders such as ionCube are involved, do not jump several branches at once; move one minor version at a time and verify that the loader works before taking the next step.

How to check your version

  • php -v shows the CLI version. That is not necessarily what serves your web traffic.
  • phpinfo() (temporarily, on a protected path, removed afterwards) shows the version and the Server API actually handling web requests, for example FPM/FastCGI or Apache 2.0 Handler. If you run several FPM versions side by side, check each pool.
  • The package changelog (commands above) shows whether the CVEs are actually fixed in a distribution build, which a version number alone may not reveal when fixes are backported.

What to do now

  • Inventory every server and container that runs PHP for web requests, including the ones nobody remembers.
  • Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or a vendor build whose changelog names both CVEs.
  • Restart every PHP worker (FPM, Apache, LiteSpeed, FrankenPHP) so the new binary is actually running.
  • Verify via the Server API in a web request, not only php -v.
  • On 8.1 or older: get a vendor backport or plan the upgrade now. Check whether locking modules can be updated.
  • Until patched: apply the advisory's stopgaps in the correct configuration layer, and make sure the origin is not reachable around your CDN.
  • Watch for unexplained PHP worker restarts, memory-limit errors and HTTP 500s, which Sansec lists among the signals worth monitoring.
  • If a server was exposed and unpatched after the early attacks began, treat it as an incident question, not only a patching task.

The uncomfortable part

The usual advice of "keep your applications updated" does not help here. The code that fails is the runtime underneath every PHP application, and attacks started before the fix existed. What decides the outcome is the dull work: knowing where PHP runs, verifying that the package actually contains the fix, and restarting what needs restarting.


pentesting.se, run by Adminor, continuously monitors customers' external attack surface: what is published, which services answer and how that changes over time. That does not tell you which PHP servers are vulnerable to these CVEs, but it does show where your exposed web servers are, which is the list you need today. Get in touch if you want help.

Want to see what your external attack surface actually looks like? Free health check, no credit card, two minutes.