Two Critical RCEs in PHP: CVE-2026-109618 and CVE-2026-109619
On 21 October 2026 the PHP security team released versions that fix two critical vulnerabilities: 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Both can allow unauthenticated remote code execution (RCE), and both are rated 9.2 under CVSS v4.0. The release had been announced a day earlier; when details leaked the same day, the fix shipped slightly ahead of the planned time. This post explains both vulnerabilities based on the official advisories, sums up who is at risk and what helps until you can update, and ends with what I did on my own server, step by step. There are no attack instructions here; everything is written from the defender's side. In short Vulnerabilities CVE-2026-109618 (GHSA-w8hw-w9fx-j79w): RCE via OOM corruptionCVE-2026-109619 (GHSA-6wmf-4rv5-p3rp): RCE via stale global state CVSS v4.0 9.2 (both) · AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N CWE CWE-416 (use after free), CWE-755, CWE-824, CWE-1325, CWE-1419 Affected All versions before 8.2.35, 8.3.36, 8.4.27 and 8.5.12 Fixed 8.2.35 · 8.3.36 · 8.4.27 · 8.5.12 At risk Every PHP installation serving web requests: PHP-FPM, Apache mod_php, php-cgi, FrankenPHP, LiteSpeed. Highest risk with mod_php. Only complete fix Updating. Mitigations only close specific paths. Timeline When What happened 20 Oct php.net posted "Upcoming Critical PHP Security Release": two critical vulnerabilities, CVSS v4.0 9.2, release on 21 Oct at 14:00 UTC. 20 Oct Details leaked. Sansec and Huntress published warnings the same day; the leak circulated as "php2shell". 21 Oct 13:34 UTC The two GitHub Security Advisories were published. 21 Oct 13:48 UTC The PHP CNA published both CVE records. 21 Oct 8.5.12, 8.4.27, 8.3.36 and 8.2.35 were released. Linux distributions and LTS vendors received the patches in advance. CVE-2026-109618: request memory left half-initialized by OOM When PHP runs out of memory (OOM), it uses longjmp() to leave the current context and abort the request. According to the advisory, in some contexts request memory becomes reachable before it is fully initialized. If any allocation fails at that moment, PHP doesn't account for the early exit and leaves the request memory in an invalid state. The failed allocation doesn't even need to be related to that memory. The request shutdown procedure may then read or branch on the invalid data. The attacker's job is to push a request into OOM while it stays within PHP's apparent limits. The advisory lists several multipart parsing paths that make this possible: A multipart request without a usable Content-Length can pass the initial post_max_size check and keep supplying decoded body data. A multipart part can carry an unlimited number of headers, and their in-memory representation uses far more memory than their size on the wire. Empty file controls don't count toward max_file_uploads but still create file metadata arrays, and nested field names multiply that work. It depends on the SAPI Apache mod_php: shutdown always runs, even when request startup failed. Automatic body parsing can hit the memory limit at exactly that invalid moment, and all of this happens before any application code runs. Full RCE was validated over HTTP against mod_php with default PHP input and memory limits. PHP-FPM: when startup fails, FPM exits the child, so the invalid state is never consumed. OOM can still be reached after normal startup, though. Source analysis supports FPM as susceptible; no complete FPM exploit has been demonstrated. FrankenPHP: like mod_php, it calls shutdown unconditionally, so it is likely open to application-independent attacks. LSAPI: modeled on FPM; application-specific exploits are likely but unvalidated. The patch removes the longjmp() OOM exit. It is replaced by a graceful soft OOM at the next VM safe point, plus an immediate hard OOM via _exit() if memory reaches a second, higher threshold. CVE-2026-109619: stale state carried from one request to the next A PHP worker handles many requests in sequence. Some global state (error, session, object and extension state) stays in the worker but points to memory owned by one request. According to the advisory, that state must be reset before the request memory is released. If it isn't, some operations that run before normal initialization of the next request touch freed memory: a classic use after free. Exploitation needs two pieces: A producer: an operation that stores request-owned state globally and skips its reset because of a fatal error or OOM. Parse failures, uncaught errors, a failing require and Maximum execution time exceeded are examples of such interruptions. A consumer: an operation in a later request on the same worker that reads the stale value before normal initialization. The advisory confirms two consumers: INI diagnostics before request initialization. If an invalid or deprecated setting raises a warning during startup, the stale error state is used. The warning doesn't need to be displayed or logged, so error_reporting, @ and display_errors give no protection. Session upload progress. Upload progress touches session handling before ordinary session initialization and can use session state left by an interrupted request. Two concrete cases are confirmed: fatal_error_backtraces on PHP 8.5: when enabled, fatal error reporting stores a request-owned backtrace that shutdown never resets. The confirmed mod_php exploit is built on this. The session upload progress lifecycle: with mod_php, the producer and the fatal error can both happen during multipart startup. On FPM the validated path is different: state is published while the application runs, a fatal error follows, and upload progress acts as the early consumer. It works on a normal dynamic FPM pool but depends on a suitable application operation. When is the INI path open on FPM? According to the advisory, php.ini and FPM pool values are applied when a worker starts, and .user.ini after normal initialization, so they don't provide an early consumer. But if nginx or Apache mod_proxy_fcgi sends PHP_VALUE / PHP_ADMIN_VALUE parameters to FPM and those values raise a diagnostic before initialization, FPM is exposed through this path too. Panels like CloudPanel send fastcgi_param PHP_VALUE to every site with values like memory_limit, post_max_size and date.timezone. Valid values raise no diagnostic. A deprecated or invalid one raises the risk. Who is at risk? Highest: Apache mod_php and FrankenPHP. The attack can run before application code and independently of the application. Moderate: PHP-FPM and LSAPI. There are validated paths, but most depend on the application or configuration. That is not a reason to delay the update. 8.1 and older: the PHP project no longer ships updates for these. As an exception, minimal source backports were published for people building 7.4, 8.0 and 8.1 from source (PHP-7.4, PHP-8.0, PHP-8.1 branches). If you use distribution packages, ask your vendor or move to a supported version. 8.2: reaches end of life on 31 December 2026. Install this fix, then plan the upgrade. The impact is code execution as the web worker account: an attacker could run commands on your server with the privileges of the user PHP runs as. How to check your server # Installed versions and package versions (Debian/Ubuntu) ls /etc/php/ php -v dpkg -l | grep -E '^ii php[0-9.]+-(fpm|cli|common) ' # FPM services that are actually running systemctl list-units --type=service 'php*-fpm*' # Is Apache mod_php loaded? (highest risk) apache2ctl -M 2>/dev/null | grep -i php # Does the web server send PHP_VALUE to FPM? grep -rn "PHP_VALUE\|PHP_ADMIN_VALUE" /etc/nginx/ /etc/apache2/ 2>/dev/null # Is a fixed version in your repository yet? apt-get update && apt-cache policy php8.4-fpm CLI and FPM settings can differ. Use php-fpm8.4 -i to see what FPM actually uses. Updating Distributions and LTS vendors received the patches in advance, so distribution packages should arrive quickly. The advisory adds two notes: the volunteer-maintained rpms.remirepo.net repository will not have updates today, so switch to distribution packages or build from source if you rely on it. Windows binaries are on the php.net downloads page. # Record your sites' status first for d in site1.com site2.com; do echo "$d $(curl -s -o /dev/null -w '%{http_code}' https://$d/)"; done # Upgrade only the installed PHP packages, keep config files sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install --only-upgrade -y \ -o Dpkg::Options::=--force-confold $(dpkg -l | awk '/^ii php/{print $2}') # Verify php8.4 -v sudo php-fpm8.4 -t && systemctl is-active php8.4-fpm Docker: pin the official php image to the fixed version (for example php:8.4.27-fpm) and recreate the containers. Floating tags like 8.4-fpm only update after a docker pull. Shared hosting: if you have no server access, write to your provider today and ask which version you are on and when the fix will be installed. If you can't update yet: the official mitigations The php.net advisory is explicit: these only reduce exposure to specific attack paths and don't make an installation safe. For CVE-2026-109618 (OOM): Set memory_limit=-1 and enforce it where applications can't override it, such as php_admin_value in the FPM pool. Check that no pool, directory or application setting restores a finite limit. This removes the controllable OOM, but an underlying mmap() failure can still reach the same path. Unlimited memory is also a denial of service risk on a shared server, so do it deliberately. Keep post_max_size low: 8M for applications that don't take uploads, 32M if they do. Keep upload_max_filesize below it. At the web server or proxy, limit decoded request size, input field count, multipart part count, header count and nesting depth, including requests without a usable Content-Length. If your application doesn't use multipart forms, block them before they reach PHP. A container or cgroup memory limit can act as an availability boundary. Don't use RLIMIT_AS or RLIMIT_DATA: they cause exactly the allocation failure that triggers the vulnerable path. For CVE-2026-109619 (stale state): Set session.upload_progress.enabled=0 and restart every worker. Put it in php.ini, an FPM pool or server configuration applied before request parsing. ini_set() and FPM .user.ini don't work. This blocks the confirmed session paths, not other variants. On PHP 8.5, set fatal_error_backtraces=0 and restart every worker. This blocks only the confirmed backtrace path. Remove invalid or deprecated settings applied per request (.htaccess, Apache php_value, nginx PHP_VALUE). Fix fatal errors and execution timeouts an attacker can trigger, including in callbacks, upload or import processing and cleanup. ; /etc/php/8.5/fpm/conf.d/99-mitigation.ini session.upload_progress.enabled = 0 fatal_error_backtraces = 0 ; PHP 8.5 only sudo php-fpm8.5 -t && sudo systemctl reload php8.5-fpm php-fpm8.5 -i | grep -E '^(session.upload_progress.enabled|fatal_error_backtraces)' Does a Cloudflare rule help? A Cloudflare WAF rule shared after the leak blocks requests whose body carries a PHP_SESSION_UPLOAD_PROGRESS field: any(http.request.body.form.names[*] == "PHP_SESSION_UPLOAD_PROGRESS") or http.request.body.raw contains "PHP_SESSION_UPLOAD_PROGRESS" Action: Block. But it only targets the upload progress consumer; it doesn't cover the OOM vulnerability or the INI path. It has two more limits: Cloudflare only inspects the first part of the request body. If the field comes later, the rule misses it. If your server can be reached directly by its IP address, the attacker never passes through Cloudflare. I tested the second one on my own server, and every site, including my own, opened directly by IP. My main site looked closed at first because the server uses a self-signed certificate and curl refused the connection. With -k it returned 200. An attacker won't care about the certificate either: curl -sk -o /dev/null -w "%{http_code}\n" --resolve yoursite.com:443:SERVER_IP https://yoursite.com/ If that returns 200, your origin is exposed. Accept only Cloudflare's IP ranges on the server, or use Authenticated Origin Pulls. What I did on my own server My server runs CloudPanel on Ubuntu 26.04. Every site runs on PHP-FPM behind nginx; there is no Apache or mod_php. Time (UTC+3) Step 11:26 Inventory: ten versions installed, from 7.1 to 8.5, but only the 8.4 and 8.5 FPM services running. Versions 8.4.22 and 8.5.9. 11:26 The real surprise: on 8.4 I was already three security releases behind before the emergency fix (8.4.23, 8.4.24, 8.4.26). 11:59 Recorded the HTTP status of every site, then installed the latest available releases: 8.4.26 and 8.5.11 (188 packages, over half an hour). Every site answered the same as before. 12:01 session.upload_progress.enabled=0 on 8.4 and 8.5, FPM reloaded. 16:48 Read the advisories. nginx sends PHP_VALUE to every site; all values are valid and there is no open_basedir, so they raise no INI diagnostic. 16:48 fatal_error_backtraces was on in 8.5. Turned it off, reloaded FPM, checked the sites again. pending CloudPanel's package repository doesn't have 8.4.27 and 8.5.12 yet. I'll install them the moment they land and update this post. I didn't apply memory_limit=-1. There are several sites on this server, and unlimited memory would let one bad request slow down all of them. No complete FPM exploit has been demonstrated and the package is close, so I decided against it. On mod_php I would have decided differently. I also covered this in a short video (in Turkish): Pixel Shorts · Emergency patch for PHP. Credits According to the advisories, the vulnerabilities were reported by lcfr-hax, sagitzadik-wiz, tomash-areion and amyb-oai. The fixes were developed by arnaud-lb, iluuu1994, bukka and ndossche. References php.net: Security advisory, critical remote code execution vulnerabilities (21 Oct 2026) GHSA-w8hw-w9fx-j79w: RCE via OOM corruption (CVE-2026-109618) GHSA-6wmf-4rv5-p3rp: RCE via stale global state (CVE-2026-109619) CVE-2026-109618 record · CVE-2026-109619 record PHP 8 ChangeLog php.net: Upcoming Critical PHP Security Release (20 Oct 2026) Sansec: php2shell Huntress: php2shell, leaked critical PHP vulnerabilities Cloudflare Rules language: http.request.body.form.names Originally published at iamcanturk.dev. I wrote this post together with Claude Code.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to