Critical PHP RCEs (CVE-2026-109618, CVE-2026-109619): Patch Paths and Mitigations
Two unauthenticated RCEs fixed in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. What the bugs are, who is exposed, how to patch distro and Docker installs, and the php.ini, FPM, Apache and proxy mitigations to apply until you can.
On 21 October 2026 the PHP project shipped security releases for two critical vulnerabilities, CVE-2026-109618 and CVE-2026-109619, both rated 9.2 under CVSS v4.0. Both allow unauthenticated remote code execution against PHP installations that serve web requests, and according to Cloudflare the issue leaked before the coordinated release and attack traffic is already being observed. If you run PHP behind a web server, this is a patch-today event.
This article will be updated as the situation evolves. All times are UTC.
Updates (newest first)
2026-10-22 07:15 UTC — Surprise of the night: Ondřej Surý, who maintains the
deb.sury.orgDebian repository and theppa:ondrej/phpUbuntu PPA, backported the fix to PHP 5.6 and PHP 7.4. 5.6 goes beyond the PHP project’s own source backports, which stop at 7.4. The changelog of both packages reads “Backported from 8.2.35 — [CVE-2026-109618, CVE-2026-109619]: Avoid state corruption via bailout.” Fixed versions:php5.65.6.40-104 andphp7.41:7.4.33-30 or later (currently 7.4.33-31), built for Debian on 21 October and published to the PPA for Ubuntu 24.04 and 22.04 at 14:45 UTC. If you still run legacy PHP on Debian or Ubuntu through that repository,apt update && apt upgradeand restart PHP-FPM or Apache. Check withdpkg -l 'php5.6*' 'php7.4*'andzcat /usr/share/doc/php7.4-common/changelog.Debian.gz | grep -m1 109618. This buys you time, not a support contract: plan the upgrade.2026-10-22 06:20 UTC — Derived images are following:
dunglas/frankenphp(18:17 UTC yesterday),matomo(05:11 UTC) andwordpress(06:20 UTC) have been rebuilt on the fixed PHP.matomo:5.14.0-apacheanddunglas/frankenphp:1-php8.4now report PHP 8.4.27. Same tag, new content:docker compose pullthen recreate, and check withdocker exec <container> php -v.2026-10-21 22:09 UTC — Docker: docker-library/official-images#22541 was merged at 15:16 UTC, and all four lines are now on Docker Hub (
php:8.5.12-*from 18:59 UTC,8.2.35,8.3.36and8.4.27between 21:59 and 22:09 UTC). Rebuild withdocker build --pullor bump your pinned tag, then recreate the containers.2026-10-21 15:05 UTC — Docker: the official-images PR that bumps
phpto 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 (docker-library/official-images#22541, opened at 14:29) is still open with CI running. Tags such asphp:8.4.27-apacheorphp:8.5.12-fpmare not on Docker Hub yet. The upstream Dockerfile update (docker-library/php#1694) was merged at 14:19. If you build on official images: apply the ini mitigations below now, then rebuild withdocker build --pullor bump your pinned tag once the tags land. Images built on top of them (matomo:*-apache,wordpress,dunglas/frankenphpand so on) need their own rebuild after that. The PR thread has been locked after a flood of comments, so watch it rather than posting to it.2026-10-21 14:00 UTC — PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 released as security releases.
2026-10-21 13:56 UTC — Cloudflare publishes its write-up (“php2shell”). WAF rules have been live for Free and paid plans since 03:02, roughly nine hours before disclosure. Cloudflare says the vulnerability leaked before the official release date, that attack traffic was observed, and that attackers were mutating payloads to bypass rules.
2026-10-20 — The PHP security team pre-announces a critical release for 21 October at 14:00 UTC: two vulnerabilities, both CVSS v4.0 9.2.
TL;DR
- What: two unauthenticated RCEs in PHP’s request handling. CVE-2026-109618 (GHSA-w8hw-w9fx-j79w) is memory corruption reachable through out-of-memory handling. CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp) is stale global state surviving from one request into the next.
- Who: every PHP installation that serves web requests: PHP-FPM, Apache
mod_php,php-cgi, FrankenPHP, LiteSpeed. The PHP team ratesmod_phpas highest risk, with a demonstrated exploit in which an unauthenticated request triggers the attack before any application code runs. Cloudflare states that all web servers running PHP 5.4 or later are affected. - Do now: update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 (or your distribution’s patched package). If you cannot update within hours, apply the mitigations in this article, restart every worker, and verify the effective values from the web SAPI. Mitigations reduce exposure; in the PHP team’s words, updating is the only safe choice.
The two bugs, from an operator’s point of view
The vulnerabilities were disclosed by Calif.io, Wiz, OpenAI and Quarkslab. The PHP advisory deliberately stays high level, and so does this section. No exploit details are public from the vendors, and none are needed to make good decisions. What matters is where in the request lifecycle the bugs live.
A PHP web worker (an FPM child, an Apache process with mod_php loaded, a FrankenPHP thread) goes through the same cycle for every request: initialise per-request state, parse the incoming request (headers, query string, body, multipart uploads, session-related input), run the application script, then tear everything down and get ready for the next request. Long-lived workers handle thousands of requests in a row, and the whole design relies on startup and cleanup being airtight.
CVE-2026-109618: RCE via out-of-memory corruption. Out-of-memory handling can interrupt PHP while internal request data is only partially initialised. Request cleanup may then treat that half-built data as valid. Cleanup code acting on invalid structures is a classic route from a crash to memory corruption, and from there to code execution. The attacker’s lever is memory pressure: Cloudflare notes that large requests are one way to create it, and the configured memory_limit is the most controllable failure point.
CVE-2026-109619: RCE via stale global state. Request-owned internal state can survive after the memory backing it has been freed. A later request handled by the same worker may use that stale state before normal initialisation resets it. In other words, one request can leave a dangling reference behind, and the next request through that worker can be the one that gets exploited.
Two consequences follow:
- Persistent workers matter. The second bug is a cross-request problem, and it needs a process that serves more than one request. That is the normal state of FPM pools,
mod_phpunder prefork and every modern PHP application server. - “Before any application code runs” is the worst case. Your framework, your WAF plugin, your authentication middleware and your input validation all execute inside the application script. If the attack completes during request parsing or early initialisation, none of that code ever gets a chance to see the request. Updating your CMS, plugins or framework does not fix this, and Cloudflare says the same.
Am I affected?
Assume yes until you have checked each runtime. Start with the version:
php -v
On a server with several PHP versions installed, check each one (php8.3 -v, php8.4 -v, and so on) and the FPM binaries (php-fpm8.3 -v, or /usr/sbin/php-fpm -v on RHEL-family systems).
Then identify how PHP serves the web, because that tells you which config files apply:
# Apache with mod_php
apachectl -M 2>/dev/null | grep -i php
# PHP-FPM pools
ls /etc/php/*/fpm/pool.d/ 2>/dev/null; ls /etc/php-fpm.d/ 2>/dev/null
# Running PHP processes
ps -eo pid,user,cmd | grep -E 'php|frankenphp|lsphp' | grep -v grep
Containers carry their own PHP and are easy to forget. List them and ask each one:
docker ps --format '{{.Names}}\t{{.Image}}'
docker exec <container> php -v
Packaged distributions report the upstream version plus a distro suffix, and distro security fixes are often backported without changing the upstream number. Check the package changelog or security tracker for the CVE IDs instead of comparing version strings only:
# Debian / Ubuntu
apt list --installed 2>/dev/null | grep -E '^php'
apt changelog php8.3-fpm 2>/dev/null | grep -E 'CVE-2026-10961[89]'
# RHEL / Alma / Rocky / Fedora
rpm -qa 'php*'
rpm -q --changelog php-fpm | grep -E 'CVE-2026-10961[89]'
Patch paths
Distribution packages. Distributions and LTS vendors received patches in advance of the disclosure. Update through your normal channel (apt upgrade, dnf upgrade) and restart PHP-FPM or Apache afterwards. A package upgrade that does not restart the running workers leaves the old code in memory.
Official Docker images. The fixed tags (php:8.5.12, 8.4.27, 8.3.36, 8.2.35 and their variants) have been on Docker Hub since the evening of 21 October. Rebuild with docker build --pull (or bump your pinned tag) and recreate the containers. Remember that images built on top of php (wordpress, matomo, dunglas/frankenphp, your own base image, your CI runners) only pick up the fix when their own maintainers, or you, rebuild them. Apply the ini mitigations to those containers in the meantime.
remirepo. rpms.remirepo.net will not have updates today. The PHP project recommends switching to distribution packages or building from source if you depend on it.
PHP 8.1 and older. These branches are end of life and get no upstream releases. The security team has nevertheless provided minimal source backports for 7.4, 8.0 and 8.1. On Debian and Ubuntu, the deb.sury.org repository and ppa:ondrej/php ship fixed packages for 7.4 and even 5.6 (see the update at the top). If you run one of those, apply the backport (or your LTS vendor’s package), and treat this as the moment to schedule the upgrade you have been postponing. PHP 8.2 itself reaches end of life on 31 December 2026.
Mitigations when you cannot update immediately
These come from the PHP advisory. Each one narrows a known path. None of them closes every variant, and some have side effects you need to plan for.
For CVE-2026-109618 (out-of-memory corruption)
- Remove the controllable memory-limit failure: set
memory_limit = -1, ideally in a way the application cannot override (php_admin_value), and check that nothing else puts a finite limit back (another pool file, a framework bootstrap callingini_set). An underlying allocation failure can still reach the vulnerable path, so this reduces exposure but does not remove it. - Keep request bodies small:
post_max_size = 8Mif the application takes no uploads,32Mif it does, andupload_max_filesizebelowpost_max_size. - Enforce request limits in front of PHP: decoded request size, number of input fields, number of multipart parts, header count and nesting depth, including for requests without a usable
Content-Length(chunked bodies). - Use a container or cgroup memory limit as your availability boundary, preferably one that kills only the affected worker. Do not use
RLIMIT_ASorRLIMIT_DATA(ulimit -v,ulimit -d,LimitAS=/LimitDATA=in systemd units): they can cause exactly the allocation failure the bug needs.
There is a real tension here. memory_limit normally protects the host from a runaway script. With -1, a single request can grow until something outside PHP stops it. Before you flip it, make sure that “something” exists: a container mem_limit, a systemd MemoryMax= on the FPM unit, or a sane pm.max_children so that the worst case fits in RAM. Expect an occasional OOM-killed worker rather than a clean PHP fatal error, and watch for it in your logs.
For CVE-2026-109619 (stale global state)
- Disable session upload progress:
session.upload_progress.enabled = 0, set throughphp.ini, the FPM pool or server configuration that applies before request parsing, then restart every worker.ini_set()and FPM’s.user.inifiles are too late: by the time they apply, the request has been parsed. This blocks the confirmed session paths, not other variants. - On PHP 8.5 only:
fatal_error_backtraces = 0, then restart workers. This blocks the confirmed fatal-backtrace path only. - Remove invalid or deprecated per-request settings that emit diagnostics before initialisation completes. Hiding those diagnostics with
error_reporting,display_errorsor@does not help; the setting itself has to go. - Fix attacker-reachable fatal errors and execution timeouts in callbacks, upload and import processing, and cleanup code. Anything an unauthenticated user can push into a fatal error or
max_execution_timeis part of the attack surface.
php.ini (any SAPI)
Drop a dedicated file in the SAPI’s conf.d directory (for example /etc/php/8.3/fpm/conf.d/99-cve-2026-10961x.ini on Debian, /etc/php.d/99-cve-2026-10961x.ini on RHEL), so it is easy to find and remove later:
; Temporary mitigations for CVE-2026-109618 / CVE-2026-109619
; Remove once PHP is updated to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35.
session.upload_progress.enabled = 0
memory_limit = -1
post_max_size = 8M ; 32M if you need uploads
upload_max_filesize = 6M ; keep below post_max_size
; PHP 8.5 only:
; fatal_error_backtraces = 0
A php.ini value can still be changed by ini_set() (for memory_limit) or by per-directory configuration. To make values stick, use the admin variants below.
PHP-FPM pool
In each pool file (/etc/php/8.x/fpm/pool.d/www.conf, or an extra file such as zz-mitigation.conf containing the same [www] section header):
[www]
php_admin_flag[session.upload_progress.enabled] = off
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 6M
; PHP 8.5 only:
; php_admin_flag[fatal_error_backtraces] = off
php_admin_* values cannot be overridden by ini_set() or .user.ini. Then restart, not just reload, so that every child is replaced:
php-fpm8.3 -t && systemctl restart php8.3-fpm
Apache with mod_php
Put the flags in server or virtual-host configuration, not in .htaccess. php_admin_flag and php_admin_value are only accepted there, and a later php_flag in an .htaccess file cannot override them:
# /etc/apache2/conf-available/php-cve-2026.conf (a2enconf php-cve-2026)
<IfModule php_module>
php_admin_flag session.upload_progress.enabled off
php_admin_value memory_limit -1
php_admin_value post_max_size 8M
php_admin_value upload_max_filesize 6M
</IfModule>
On older setups the module identifier may be php7_module. Run apachectl configtest, then fully restart Apache (systemctl restart apache2 or httpd), since every child process holds its own copy of PHP.
Containers and FrankenPHP
The official php images, and images derived from them such as dunglas/frankenphp and the Apache-based CMS images, read extra ini files from /usr/local/etc/php/conf.d/. Mount the ini file from above read-only and recreate the container:
services:
app:
volumes:
- ./php-cve-2026.ini:/usr/local/etc/php/conf.d/zz-cve-2026.ini:ro
mem_limit: 1g # the availability boundary once memory_limit is -1
For Apache-based images, also mount the php_admin_* config above into /etc/apache2/conf-enabled/. For FPM-based images, mount the pool snippet into /usr/local/etc/php-fpm.d/.
Request limits at the proxy (nginx example)
client_max_body_size 8m; # match post_max_size
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k; # caps header size and count of large headers
PHP has its own limits on parsed input that are worth tightening at the same time: max_input_vars, max_input_nesting_level, max_file_uploads and max_multipart_body_parts. Keep them close to what your application actually needs.
Verify the effective values from the web SAPI
The CLI reads a different php.ini than FPM or Apache, so php -i on the shell proves nothing about the web workers. Serve a temporary script through the same vhost as the application, with an unguessable name, and delete it afterwards:
<?php
// /var/www/app/public/ini-check-7f3a9c.php - delete after use
header('Content-Type: text/plain');
foreach (['session.upload_progress.enabled', 'memory_limit', 'post_max_size',
'upload_max_filesize', 'fatal_error_backtraces'] as $k) {
echo $k, ' = ', var_export(ini_get($k), true), "\n";
}
echo 'PHP ', PHP_VERSION, ' / ', PHP_SAPI, "\n";
curl -s https://example.com/ini-check-7f3a9c.php && rm /var/www/app/public/ini-check-7f3a9c.php
You want session.upload_progress.enabled = '0' (or '') and memory_limit = '-1', reported by fpm-fcgi, apache2handler or frankenphp, not cli.
Field note. On a mod_php Matomo container we disabled upload progress with a mounted conf.d ini plus an Apache php_admin_flag in server configuration. We then placed an .htaccess containing php_flag session.upload_progress.enabled on in the document root and requested the check script: the web SAPI still reported 0. That is the behaviour you want to confirm on your own stack, because it proves that a compromised or careless application directory cannot quietly turn the mitigation off.
The WAF layer (Cloudflare)
Cloudflare deployed rules at 03:02 UTC, before disclosure, for both Free and paid plans. They are in the Cloudflare Managed Free Ruleset (77454fe2d30c4220b5701f6fdfb893ba) and the Cloudflare Managed Ruleset (efb7b8c949ac4650a09736fc376e9aee) as seven rules named Generic Rules - Anomalous Execution - 1 to 7.
| Rule | Free ruleset ID | Managed ruleset ID |
|---|---|---|
| 1 | 4e315666e8e344cdb64119a3b5ccf32f | c1d409ad838f47f59446f21cc438d5dd |
| 2 | 00c10e6cc4da4bd08722adeebcd95984 | f9a45927414245e3a64bb12d0a0c2f64 |
| 3 | 48ddf592361e4fdb97a8a85982dab691 | 771c654ae1154d4daa72f4e1545d163c |
| 4 | 3556683d512a47f2937899c524b1a4c6 | 217005865f0145f392300bac212afcc9 |
| 5 | da07c85852e34625bb478496302ede46 | 38efdb17ea594501bb55a1d71867b12e |
| 6 | 19373a83abb840e18ab3ce5a7975ce1b | 4148ae686fdb4d01be673fab9d9c073d |
| 7 | 07f0b6ddaabf40b1b2042dade8d613fa | 1389aa259617437392ce149c48143645 |
On Pro, Business and Enterprise plans, confirm that these rules are in block mode rather than log. A response-side rule, afffe22785674c80a31875c848d2242a (Default block for High confidence Signatures, in the Managed Response Firewall Default ruleset, all plans), uses full transaction detection: the request and the origin response are analysed together, and the origin’s response can be blocked. To see whether you are being probed, go to Security > Security Analytics > Events and filter on the rule IDs above.
Treat this as defence in depth, not a fix. Cloudflare itself notes that attackers are mutating payloads to evade the rules, and a WAF only covers traffic that actually goes through it. If your origin accepts connections that bypass the proxy, lock it down to the CDN’s IP ranges as well.
Checklist
- Inventory every PHP runtime: hosts, every installed version, every container, CI images.
- Update to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, or the distro package that lists both CVEs. Apply the source backport on 7.4 / 8.0 / 8.1.
- Restart every PHP worker (FPM, Apache, FrankenPHP, LiteSpeed) after the update.
- Where you cannot update today:
session.upload_progress.enabled=0,memory_limit=-1with a container or cgroup memory limit, smallpost_max_size,fatal_error_backtraces=0on 8.5, all viaphp_admin_*or server config. NoRLIMIT_AS/RLIMIT_DATA. - Tighten request limits at the proxy and PHP’s own input limits.
- Verify the effective values from the web SAPI, then delete the check script.
- Rebuild derived Docker images once the official tags are published.
- If you are behind Cloudflare, confirm the managed rules are in block mode and review the events.
- Remove the temporary mitigations once patched, and restore a finite
memory_limitif you prefer it.
Sources
- PHP security announcement, 21 October 2026: php.net/archive/2026.php#2026-10-21-5
- Cloudflare, WAF protection for the PHP vulnerabilities: blog.cloudflare.com/php-vulnerabilities-waf-protection
- Docker official images, PHP bump: github.com/docker-library/official-images/pull/22541
- Ondřej Surý’s PHP packages for Debian and Ubuntu: deb.sury.org and launchpad.net/~ondrej/+archive/ubuntu/php