PHP patching playbook: two unauthenticated RCEs fixed in October 2026
Two PHP flaws allow remote code execution without login: the fixed releases, how to check running workers and why ini changes fall short.
This article is educational and does not replace a formal smart contract audit, legal advice, financial advice, or protocol-specific security review.

On 21 October 2026 the PHP project shipped fixes on its four supported branches for two critical flaws, CVE-2026-109618 and CVE-2026-109619, that can allow remote code execution without authentication. The fixed releases are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. The release announcements are direct: there are no complete mitigations, and updating is the only complete fix. The GitHub advisory for the second flaw says the same in other words: updating to a release with the complete patch is the only reliable mitigation.
For organisations that run older PHP branches or cannot update on release day, the playbook also sets out the temporary measures and what they leave open.
This playbook covers three things: finding every PHP that serves web requests, confirming the version each running process reports after the restart, and the temporary measures for anyone who cannot update today.
The ini measures listed by php.net reduce exposure to specific attack paths, and php.net itself says they do not fix the installation. They are a bridge of hours or days until the fixed version is running, not a substitute. And a server is patched when its running workers report the fixed version after a restart, not when a package has been installed.
What was fixed
The two flaws, as php.net describes them:
- CVE-2026-109618 (GHSA-w8hw-w9fx-j79w), remote code execution through out-of-memory corruption: out-of-memory handling can interrupt PHP while internal request data is only partially initialized, and the request cleanup may then treat the invalid data as valid.
- CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp), remote code execution through stale global state: internal state owned by one request can survive after its memory has been freed, and a later request handled by the same worker may use it.
Both GitHub advisories rate them Critical, 9.2 in CVSS v4.0: network attack vector, no privileges and no user interaction, but high attack complexity and attack requirements present. Every version below 8.2.35, 8.3.36, 8.4.27 and 8.5.12 on each branch is affected. php.net had given advance notice of the release on 20 October, with that rating.
The ChangeLog for 8.4.27 sums up the fix in one line: “Avoid state corruption via bailout”. PHP 8.6.0 RC4, released the same day, also includes the fixes; php.net recalls that it is a test version and not meant for production.
Who is at risk
php.net puts it this way: “all PHP installations serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed”. It considers the risk highest for mod_php, where a demonstrated exploit lets an unauthenticated request trigger the attack “before any application code runs”.
That detail matters. With mod_php, the quality of the application code does not change the exposure: a clean codebase does not protect a vulnerable interpreter.
The advisories add detail by environment:
- First flaw: complete remote code execution was validated over HTTP against mod_php with default input and memory limits. The advisory's source analysis places FrankenPHP close to mod_php, and LiteSpeed's LSAPI close to FPM, where exploits that depend on the application are likely but were not validated.
- Second flaw: the session path was validated against mod_php and FPM.
- In both: more workers, unrelated traffic or worker recycling lower the odds of each attempt, but they do not remove the condition.
The second advisory carries a warning for incident response: the stale state can be published or consumed “without a predictable log entry”. Clean logs are not evidence that nothing happened.
Step 1: find every PHP that runs
A server can have more than one PHP: the CLI, one or more FPM versions, a php-cgi left over from an old setup, a LiteSpeed lsphp, a FrankenPHP binary or an Apache module. Each one is updated on its own. On Linux, this command lists the PHP binaries and the version each one reports:
find / \( -path /proc -o -path /sys -o -path /var/lib/dpkg \) -prune -o -type f -perm -u+x \
\( -name php -o -name 'php[0-9].[0-9]' -o -name 'php[0-9][0-9]' -o -name php-cgi -o -name 'php-cgi[0-9].[0-9]' \
-o -name php-fpm -o -name 'php-fpm[0-9].[0-9]' -o -name lsphp \) -print 2>/dev/null |
while read -r b; do echo "$b: $("$b" -v 2>/dev/null | head -1)"; doneOn the test machine, a Debian 13, it found two: /usr/bin/php8.4 (cli) and /usr/sbin/php-fpm8.4 (fpm-fcgi), both 8.4.27. FrankenPHP and mod_php do not show up. FrankenPHP is a single binary with its own subcommand for the version. mod_php is a library that Apache loads, so it is checked in the web server's module list, for example with “apachectl -M” or, on Debian, “a2query -m”.
Containers carry their own PHP. The command has to run inside each image, not only on the host.
Step 2: the package version is not the upstream version
According to php.net, Linux distributions and LTS vendors received the patches in advance, so the update usually arrives through the package manager. The package version belongs to the distribution and does not always follow upstream numbering: a vendor can carry the fix in a build with an older version number. The reference is your vendor's advisory for these two CVEs, not the number alone. On Debian or Ubuntu:
dpkg-query -W -f='${Package} ${Version}\n' 'php*-fpm' 'php*-cli' 'php*-cgi' 'libapache2-mod-php*' 2>/dev/nullOn 25 October, Debian 13 served php8.4-fpm 8.4.27-1~deb13u1 on the test machine. On RPM-based systems, the equivalent is “rpm -q” with the package name.
Step 3: check what is running, not what is installed
Installing the package replaces the file on disk. Processes that are already running keep the old code in memory until they restart. php.net asks for every worker to be restarted after applying its measures, and the same goes for the update.
This command lists PHP and web server processes, when each one started and which binary it runs. The last line finds processes that still map a PHP binary or library that has since been deleted or replaced:
for p in $(pgrep -f 'php-fpm|php-cgi|lsphp|frankenphp|apache2|httpd'); do
echo "$p | started $(ps -o lstart= -p "$p") | $(sudo readlink "/proc/$p/exe")"
done
sudo grep -lE '(php|libphp)[^ ]* \(deleted\)' /proc/[0-9]*/maps 2>/dev/nullIn the test, the package was reinstalled while FPM was running, with the automatic restart disabled. This was the output:
678767 | started Sun Oct 25 10:49:55 2026 | /usr/sbin/php-fpm8.4 (deleted)
678768 | started Sun Oct 25 10:49:55 2026 | /usr/sbin/php-fpm8.4 (deleted)
678769 | started Sun Oct 25 10:49:55 2026 | /usr/sbin/php-fpm8.4 (deleted)
/proc/678767/maps
/proc/678768/maps
/proc/678769/maps“(deleted)” means that the process is still running the binary that existed before the update. After restarting FPM, the same command showed the new processes without that mark, and the last line printed nothing. For mod_php the check is the same on the Apache processes, because the libphp library is mapped inside them.
The most direct verification is to ask the pool itself. This snippet sends a request to FPM through its socket, without going through the web server, and prints the version and the values the pool really applies. The socket path depends on the distribution and the version; this is Debian's for 8.4:
sudo tee /run/phpcheck.php >/dev/null <<'PHP'
<?php
echo PHP_VERSION, " ", PHP_SAPI, "\n";
foreach (['memory_limit', 'post_max_size', 'upload_max_filesize', 'session.upload_progress.enabled'] as $k) {
echo $k, " = ", var_export(ini_get($k), true), "\n";
}
echo "ini_set memory_limit: ", var_export(ini_set('memory_limit', '128M'), true), "\n";
PHP
sudo SCRIPT_FILENAME=/run/phpcheck.php REQUEST_METHOD=GET \
cgi-fcgi -bind -connect /run/php/php8.4-fpm.sock
sudo rm /run/phpcheck.phpThe test file is created outside the web root and deleted at the end, so it is never reachable over HTTP. A phpinfo page or a version page should not stay published.
The CLI does not stand in for the pool. On the same machine, php on the command line reported memory_limit -1 and session.upload_progress.enabled 1, while the unchanged FPM pool reported 128M and 1. Each SAPI reads its own configuration.
Step 4: if you cannot update today
php.net is explicit that these measures reduce exposure to specific attack paths and do not fix the installation. For the first flaw:
- memory_limit=-1, applied so that applications cannot override it, for example with php_admin_value in the FPM pool or in the Apache configuration. Check that no pool, directory or application puts a finite limit back. php.net warns that an underlying allocation failure can still reach the vulnerable path.
- A low post_max_size: 8M if the application does not accept uploads, 32M if it does, with upload_max_filesize below it. php.net also suggests limits at the web server or proxy for decoded request size, number of input fields, multipart parts, headers and nesting depth, including requests without a usable Content-Length.
- Without a PHP memory limit, the availability boundary has to be a container or cgroup limit, preferably one that kills only the affected worker. php.net advises against using RLIMIT_AS or RLIMIT_DATA instead, because they can cause the vulnerable allocation failure.
For the second flaw:
- session.upload_progress.enabled=0 and a restart of every worker, set in php.ini, in the FPM pool or in server configuration that applies before request parsing. Not with ini_set() or an FPM .user.ini file. It blocks the confirmed session paths, not other variants.
- On PHP 8.5, fatal_error_backtraces=0 and a restart of every worker. It blocks the confirmed fatal backtrace path only.
- Remove invalid or deprecated settings applied per request that emit diagnostics before initialization. Hiding diagnostics with error_reporting, display_errors or @ does not help.
- Fix fatal errors and execution timeouts an attacker can reach, including those in callbacks, upload or import processing and cleanup.
In an FPM pool, the settings look like this. The upload_max_filesize value of 4M is an example; what matters is that it stays below post_max_size:
; Temporary measures for CVE-2026-109618 and CVE-2026-109619.
; Remove memory_limit and post_max_size once the fixed version is running.
; Keep session.upload_progress.enabled off until the next PHP release (CVE-2026-115560).
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 4M
php_admin_flag[session.upload_progress.enabled] = offThen test the configuration and restart:
sudo php-fpm8.4 -t && sudo systemctl restart php8.4-fpmIn the test, with these lines at the end of the www pool, the configuration test passed, and the pool reported memory_limit -1, post_max_size 8M, upload_max_filesize 4M and session.upload_progress.enabled 0. A call to ini_set for memory_limit from the script returned false: the application cannot override php_admin_value. The restart was done by hand, because the test machine has no systemd.
Each of these changes is a deviation from your baseline. memory_limit=-1 in particular removes a limit that many hardening guides set on purpose. Record each change with its reason and a removal date, and remove it once the fixed version is running.
CVE-2026-115560: one setting stays
On 23 October php.net published an informational advisory. It recommends keeping session.upload_progress.enabled=0 until the next regular bugfix release, because of an additional remote code execution vector that it describes as unlikely, CVE-2026-115560. If the setting was applied on 21 October, it stays in place and nothing else is needed.
According to php.net, the issue needs an unusual combination: the application sets session.serialize_handler to php_binary or php_serialize in its configuration, switches it to php at runtime with ini_set() before calling session_start(), and session upload progress is still enabled. Applications that never change the serialize handler at runtime can keep the feature enabled.
The full advisory for CVE-2026-115560 has not been published yet: php.net says it will come with the next release. Until then there is no list of affected or fixed versions to cite.
In practice, after the update the memory_limit and post_max_size deviations can go, but the upload progress switch stays off until that release.
PHP 8.1 and older: an exception, not a plan
PHP 8.1 and older no longer receive updates from the PHP project, and some distributions and LTS vendors provide backported fixes. Given the severity, the PHP security team made an exception: minimal source backports for three end-of-life branches, to help people who build from source or have no access to an LTS vendor. Those branches are 7.4, 8.0 and 8.1.
php.net ties that backport to the severity of these two flaws, and it does not change the end-of-life dates. 8.1 reached end of life on 31 December 2025, 8.0 on 26 November 2023 and 7.4 on 28 November 2022. The 8.2 branch receives security fixes until 31 December 2026, and the 8.2.35 announcement asks users to plan the upgrade to a supported version. If the inventory from step 1 shows 8.2 or older, the patch closes this advisory and the next task is the migration.
What to check this week
- Every PHP on every server and image: CLI, FPM, php-cgi, lsphp, FrankenPHP and mod_php, with the version each one reports.
- Which of them serve web requests, starting with mod_php.
- Whether the installed package version matches your vendor's advisory for CVE-2026-109618 and CVE-2026-109619.
- Whether any process still maps a deleted PHP binary or library.
- Whether each FPM pool reports the fixed version when asked through its socket.
- Which temporary measures were applied, where, and when they come off. session.upload_progress.enabled=0 stays until the next release.
- Which servers run 8.2 or older, and the date set for their migration.
A package in the repository or on disk is a step, not the result. For CVE-2026-109618 and CVE-2026-109619, the verification that counts is each running PHP process reporting the fixed version.
If you want to discuss how to apply these checks to your own servers or pipelines, you can write to us from the contact page; in the message, mention this article and the environment you have in mind, without credentials, tokens or confidential operational material.
Source registry
Sources consulted on 25 October 2026; php.net was checked again at 10:42 (Madrid time) before writing. Run on Debian 13 with php8.4-cli and php8.4-fpm 8.4.27-1~deb13u1, with the output saved: the inventory, the package query, the process check before and after replacing the binary, the query to FPM with the default pool and with the temporary measures, and the configuration test. Not run: systemctl, because the test machine has no systemd, Apache with mod_php, FrankenPHP, LiteSpeed, RPM-based systems or Windows.
Need help applying this to your launch or workflow?
Request a technical review and we will map the safest first step.


