System Design Simple logoSystem Design Simple Follow on YouTube

Security

How to check the PHP build your web server runs and fix CVE-2026-109618 and CVE-2026-109619

PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix two CVSS 9.2 flaws in PHP-FPM, mod_php and php-cgi. Check the build serving traffic, update, restart, verify.

How to check the PHP build your web server runs and fix CVE-2026-109618 and CVE-2026-109619: diagrams from the video

Drafted with AI assistance, then checked and edited by a human engineer · How we write

Short answer

PHP below 8.5.12, 8.4.27, 8.3.36 or 8.2.35 has two CVSS 9.2 remote code execution flaws, CVE-2026-109618 and CVE-2026-109619, in how long-lived web workers handle request startup and cleanup. Update PHP, restart every worker, then check the version the web server reports, because the CLI can be a different build. php.ini mitigations only reduce exposure.

Key facts
CVEsCVE-2026-109618 and CVE-2026-109619, both CVSS v4.0 9.2
Fixed inPHP 8.5.12, 8.4.27, 8.3.36, 8.2.35 (php.net announcement 2026-10-21)
Affected SAPIsPHP-FPM, Apache mod_php, php-cgi, FrankenPHP, LiteSpeed; highest demonstrated risk on mod_php
Older branchesbackported patches for 7.4, 8.0, 8.1; get them from your distro or LTS vendor
Complete fixupdating PHP; php.net says the mitigations do not make an install safe
Our labCLI check on PHP 8.5.2: affected version plus 3 advisory settings found; 3 settings changed under -d overrides; no exploit tested

PHP below 8.5.12, 8.4.27, 8.3.36 or 8.2.35 has two remote code execution flaws, CVE-2026-109618 and CVE-2026-109619 (CVSS v4.0 9.2), in how long-lived web workers start and clean up requests, with Apache mod_php the most exposed. Update to the fixed release for your branch, restart every worker, and check the version the web server reports, which is not always the one php -v prints.

This guide is for protecting servers and applications you own or maintain.

Am I affected?

BranchAffectedFixed in
8.5below 8.5.128.5.12
8.4below 8.4.278.4.27
8.3below 8.3.368.3.36
8.2below 8.2.358.2.35 (end of life 2026-12-31)
8.1, 8.0, 7.4all upstream releasesno project release; backported patches through your distro or LTS vendor

The php.net advisory names five SAPIs (the server interface PHP runs under): PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed. Check the binary that serves requests, not the one on your PATH:

# PHP-FPM (Debian/Ubuntu name the binary per branch)
php-fpm -v || php-fpm8.3 -v
# Apache: is mod_php loaded at all?
apachectl -M 2>/dev/null | grep -i php
# Homebrew
$(brew --prefix php)/sbin/php-fpm -v
# containers
docker exec <container> php -v
# distro builds keep the old upstream number: list installed revisions
dpkg-query -W 'php*'          # Debian/Ubuntu
rpm -qa 'php*'                # RHEL family

Upstream builds below the table are affected. For distro builds, check your vendor's security advisory for the fixed package revision on your release and compare it with every installed PHP package the web service loads. apt changelog or rpm -q --changelog searches for the CVE ids are useful supporting evidence, but apt changelog can describe a newer available package, so the installed revision is what counts.

Fix it now

# Homebrew (example; Homebrew currently offers 8.5.12_1)
brew update && brew upgrade php && brew services restart php

# Debian/Ubuntu: take the security update for every php package
sudo apt update && sudo apt install --only-upgrade 'php*'
sudo systemctl restart php8.3-fpm     # or: sudo systemctl restart apache2 for mod_php

# RHEL, Alma, Rocky
sudo dnf upgrade 'php*' && sudo systemctl restart php-fpm

For containers, the Dockerfile of the PHP service must start from a fixed official tag, for example FROM php:8.5.12-fpm, php:8.4.27-apache or php:8.3.36-fpm-alpine. Then:

# Compose service with build:
docker compose build --pull <php-service>
docker compose up -d --force-recreate <php-service>
# Compose service with image: update the tag in compose.yaml first
docker compose pull <php-service> && docker compose up -d --force-recreate <php-service>

Then prove the running process is the fixed build. A reload that keeps old workers alive leaves old code in memory, and the CLI may read a different php.ini than FPM.

# every FPM process started after the upgrade?
ps -o pid,lstart,command -C php-fpm8.3   # macOS: ps -o pid,lstart,command -p $(pgrep -d, php-fpm)

# what the web SAPI reports: temporary file, request it once, delete it
echo '<?php echo PHP_VERSION, " ", PHP_SAPI, " ", php_ini_loaded_file();' > /var/www/html/v-check.php
curl -s https://your-site.example/v-check.php; echo
rm /var/www/html/v-check.php

Expect the fixed version and fpm-fcgi or apache2handler. For containers, also confirm the image digest of the running container matches the one you just built or pulled.

Why it is dangerous

PHP apps assume shared-nothing requests: each one starts clean and everything it allocated is gone at the end. Under PHP-FPM and mod_php the worker process outlives the request, so that promise depends on startup and shutdown code doing their job every time.

HTTP request -> web server -> PHP worker (reused)
   1. request startup   parse body, multipart, session upload progress   <- no app code yet
   2. your script
   3. request shutdown  free request memory, reset state
   4. same worker takes the next request

CVE-2026-109618 lives between steps 1 and 3. Multipart metadata can take far more memory than its size on the wire, so a request can hit memory_limit while startup is still building request structures. On affected builds the out-of-memory path uses longjmp(), which jumps past the code that would have finished that half-built state, and shutdown later treats it as valid. The patch defers a soft out-of-memory error to a safe point in the VM and exits with _exit() at a higher hard limit (GHSA-w8hw-w9fx-j79w).

The SAPI decides what happens next. According to the advisories, mod_php can run shutdown after request startup fails, so the invalid state is touched before any application code or authentication runs. FPM terminates a child whose startup fails, which stops that exact path. FPM remains affected through other paths, including the validated session lifecycle case, where application code runs after a successful startup.

CVE-2026-109619 crosses steps 3 and 4: memory owned by one request is freed while a pointer to it survives, and a later request on the same worker uses it (GHSA-6wmf-4rv5-p3rp). The broken component is the isolation between requests. Either flaw ends, at worst, in code execution as the worker user, with that user's file, socket and credential access.

Reproduce

Our lab, labs/security/php2shell-check-lab/php2shell-check-lab.mjs, is a read-only configuration check. It asks the local PHP for its version and for the settings php.net ties to each CVE. It sends no attack input and does not trigger either bug. We ran it on this Mac on 2026-11-06 at 11:01 UTC against Homebrew PHP 8.5.2 in the CLI SAPI with its installed php.ini.

The CLI check found an affected upstream version and three settings covered by the advisory: session.upload_progress.enabled On, memory_limit a finite 128M, and fatal_error_backtraces On (an 8.5 setting).

Experiment

The second run used the same CLI binary with three -d overrides for the php.net mitigations, plus a plain session_start() in a temp directory as the normal-use check:

CheckInstalled php.iniWith -d overrides
PHP version 8.5.2affectedaffected, update needed
session.upload_progress.enabled (109619)OnOff
memory_limit (109618)128M-1
fatal_error_backtraces, 8.5 only (109619)OnOff
normal use: session_start() in a temp dirsession oksession ok

The simple CLI session_start() check passed in both runs.

Fix

The complete fix is the update and a restart of PHP-FPM or Apache. Until then, set the mitigations where they apply before request parsing:

; php.ini or the FPM pool (pool syntax: php_admin_value[...] / php_admin_flag[...])
; not ini_set(), not .user.ini: those run too late or can be overridden
session.upload_progress.enabled = 0
memory_limit = -1            ; pool: php_admin_value[memory_limit] = -1
fatal_error_backtraces = 0   ; PHP 8.5 only
post_max_size = 8M           ; 32M if you accept uploads, upload_max_filesize below it

Report

Terminal output of our php2shell-check-lab run: node php2shell-check-lab.mjs
CountResult
Configuration findings observed (version plus advisory settings)4/4
Settings changed under -d overrides3/3
PHP version changedno
Normal session_start() unchanged1/1

The capture shows the lab's raw labels: its "REPRODUCED" means a finding was observed and "BLOCKED" means the setting changed. No exploit was tested. The script prints PARTIAL because the version row stays affected until PHP itself is updated.

What our lab does not prove: it never triggers either vulnerability, so it cannot show that 8.5.2 is exploitable or that 8.5.12 closes the flaws; that rests on the advisories. It ran in the CLI SAPI, which can load a different php.ini than PHP-FPM or mod_php. It does not cover proxy limits, cgroup limits or per-directory overrides, and we did not record an upgrade or a post-restart web SAPI check on this Mac.

If you cannot update today

php.net says these reduce exposure and do not make an install safe. Restart every worker after each change, then verify through the web SAPI with the temporary file method above, printing ini_get() for each setting.

Mitigation (php.net)Verify it took effect
session.upload_progress.enabled=0 in php.ini, pool or server configini_get('session.upload_progress.enabled') returns 0 or empty from the web SAPI
PHP 8.5: fatal_error_backtraces=0ini_get('fatal_error_backtraces') returns 0 or empty
memory_limit=-1 via php_admin_value, no pool or directory putting a limit backini_get('memory_limit') returns -1; grep -rn memory_limit across pools, vhosts and .user.ini
post_max_size 8M (32M with uploads), upload_max_filesize below itini_get on both through the web SAPI; PHP empties $_POST and $_FILES above the limit rather than rejecting (manual)
Request size, field, part, header and nesting limits at the web server or proxy, also for requests without Content-Lengthsend an oversized request, including a chunked one, to your own staging host and expect HTTP 413 from the proxy
Container or cgroup memory limit, never RLIMIT_AS or RLIMIT_DATAdocker inspect -f '{{.HostConfig.Memory}}' <container> or cat /sys/fs/cgroup/memory.max
Remove invalid or deprecated per-request settings, fix reachable fatal errors and timeoutsthe error log shows no startup warnings or fatal errors under normal traffic

Turning off display_errors or adding @ does not help, according to php.net. Our PHP security checklist covers the application side, and the WordPress core upgrade check is the layer above PHP if you run WordPress.

Timeline and sources

DateEvent
2026-10-20php.net announces a critical security release for the next day
2026-10-21php.net security announcement: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, both GHSA entries, backported patches for 7.4, 8.0, 8.1
2026-10-22date on the ChangeLog entries for those releases
2026-10-23php.net informational advisory on CVE-2026-115560, a non-default session serializer case; mitigation session.upload_progress.enabled=0; full advisory with the next release
2026-10-23BleepingComputer reports exploitation attempts
2026-12-31PHP 8.2 end of life

My take: with long-lived workers, a flaw in request startup or cleanup is a flaw in the isolation every PHP app relies on, and no php.ini line restores that. Update, restart every worker, and read the version from the web SAPI. The same version-then-running-process habit applies to our APT signature check and the Node.js Brotli check.

Frequently asked questions

php -v shows the fixed version. Am I done?

Not yet. The CLI and PHP-FPM or mod_php can be different packages with different php.ini files. Check the version reported by the running web SAPI, and confirm every worker started after the upgrade.

My distro package says 8.3.6, not 8.3.36. Is it fixed?

Distributions backport fixes without changing the upstream version number. Find the fixed package revision in your vendor's security advisory for your release, compare it with the installed revision of every PHP package the web service loads (dpkg-query -W or rpm -q), then restart and check the running service. A CVE line in a changelog alone does not prove the installed package has it.

After updating, should I keep session.upload_progress.enabled=0?

Keep it off if your application switches session.serialize_handler to php with ini_set before session_start. That is the non-default case php.net describes for CVE-2026-115560, with the fix due in the next release. Otherwise you can turn it back on.

Sources

  1. php.net, 2026-10-21: Security advisory, critical remote code execution vulnerabilities, update nowphp.net
  2. GHSA-w8hw-w9fx-j79w: CVE-2026-109618, out-of-memory handling during request startupgithub.com
  3. GHSA-6wmf-4rv5-p3rp: CVE-2026-109619, request state used after free by a later requestgithub.com
  4. PHP 8 ChangeLog: 8.5.12, 8.4.27, 8.3.36, 8.2.35php.net
  5. php.net, 2026-10-23: Informational advisory, session upload progress (CVE-2026-115560)php.net
  6. BleepingComputer, 2026-10-23: PHP shares mitigation for new and unpatched RCE vulnerabilitybleepingcomputer.com

phpsecurityCVE-2026-109618CVE-2026-109619PHP-FPMmod_php

System Design Simple logo
System Design Simple

Written by a software engineer and system architect with 10+ years of building production web, payment, ticketing and accounting systems in Japan and Nepal. About us