Patch PHP CVE-2026-109618 and 109619 on Ubuntu and CloudPanel

CVE-2026-109618 and 109619 let attackers run code via PHP. How I patched PHP 5.6 to 8.5 on CloudPanel and Ubuntu, and proved the site runs the fix.

Published
Read time 19 min
Stack Ubuntu · PHP-FPM · Apache · Nginx · CloudPanel
Four servers, CloudPanel on Ubuntu 24.04, Ubuntu 22.04 with Ondřej Surý's PPA, Ubuntu 24.04 and Ubuntu 20.04 with Ubuntu Pro ESM, each with its patched PHP branches, leading to a web request that reports PHP 8.3.36 on fpm-fcgi.

Two critical PHP bugs, CVE-2026-109618 and CVE-2026-109619 (CVSS 9.2), let an attacker run code on the server without logging in. Every PHP from 5.4 to 8.5.11 is affected, through PHP-FPM and Apache’s mod_php alike. According to PHP’s advisory of October 21, 2026 (opens in a new tab), there is no complete mitigation and updating is the only safe choice: to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or, for 8.1 and older, to your provider’s backport.

That week I patched a CloudPanel server and three standalone Ubuntu servers, running PHP branches from 5.6 to 8.5 between them. This note is what worked, the traps that cost me time, and how to prove that the website, not just the command line, now runs the fixed build. Still on PHP 7.4 or older? The patch buys time, not support: the companion note builds PHP 8.3 from source beside PHP 7.4 so sites can move one at a time.

TL;DR: find which PHP each site really runs, install your provider’s fixed build for that branch, restart every worker, then ask the website itself which build answers.

This applies if:

  • OS: Ubuntu 20.04, 22.04 or 24.04, alone or with CloudPanel.
  • PHP: any branch from 5.6 to 8.5, from Ubuntu, Ondřej Surý’s PPA or CloudPanel.
  • Web server: Apache with mod_php or PHP-FPM, or Nginx with PHP-FPM.
  • Access: sudo on the server, and a maintenance window for the restarts.

Am I affected?

Both bugs live in how PHP handles state between requests, so they affect PHP-FPM and Apache’s mod_php alike:

  • CVE-2026-109618: running out of memory can interrupt PHP while a request is half set up, and the cleanup then trusts the half-written data.
  • CVE-2026-109619: state from one request can outlive its memory and be used by the next request on the same worker.

Your PHP is fixed if its package is at least the version below. A later revision of the same package includes the fix too.

Where your PHP comes from Fixed versions
PHP upstream (php.net) 8.2.35, 8.3.36, 8.4.27, 8.5.12
Ubuntu 24.04 (php8.3) 8.3.6-0ubuntu0.24.04.13
Ubuntu 20.04 (php7.4, ESM) 7.4.3-4ubuntu2.29+esm5, through Ubuntu Pro
Ondřej Surý’s PPA 5.6.40-104, 7.1.33-80, 7.2.34-66, 7.3.33-35, 7.4.33-31, 8.0.30-25, 8.1.34-9, 8.3.36
CloudPanel’s own packages 8.2.35-1+clp-noble, 8.3.36-2+clp-noble, 8.4.27-1+clp-noble, 8.5.12-1+clp-noble

Who is Ondřej Surý? He maintains the PHP packages in Debian, and builds PHP for Ubuntu in his PPA, ppa:ondrej/php (Debian users get the same builds from packages.sury.org). Its package versions end in +deb.sury.org+1, which is why many guides call it “the Sury PPA”. This note writes his name, Surý. His site, deb.sury.org (opens in a new tab), lists both repositories and how to support the work.

Two things surprise people:

  • Ubuntu keeps the old version number. A patched Ubuntu 24.04 still says PHP 8.3.6 in php -v. The fix is in the package revision (-0ubuntu0.24.04.13), so always compare the full revision.
  • PHP 8.1 and older get nothing from php.net. Only a package provider’s backport fixes them. Surý backported the fix to every branch from 5.6 to 8.1, under the same changelog entry. A backport fixes these two CVEs only: the branch is still end-of-life, so plan the move to a supported one.

How do I check whether my PHP is patched?

List every installed PHP package with its version:

Terminal window
dpkg-query -W -f='${binary:Package}\t${Version}\t${db:Status-Status}\n' |
awk -F'\t' '$3 == "installed" && $1 ~ /^(php[0-9]|libapache2-mod-php)/ { print $1, $2 }'

Then ask each branch’s changelog whether it mentions the two CVEs. A count of 0 means the installed package does not list the fix:

Terminal window
zgrep -cE 'CVE-2026-10961[89]' /usr/share/doc/php*-common/changelog.Debian.gz

Read the changelog inside the package, not from apt-get changelog. On my servers that command failed for all three providers: “Changelog unavailable” for the PPA and for ESM, and a 404 for Ubuntu’s own package. The changelog also lives in the -common package: the -fpm package has none to search.

Before you start

  1. Find which PHP serves each site. php -v only shows the command-line PHP. One server can run several FPM versions and Apache’s module side by side.
  2. Back up the configuration and the package list, and take a snapshot if you can: config files alone cannot roll back binaries.
  3. Use a maintenance window. Installing and restarting workers interrupts the requests in flight.
  4. Trust the changelog, not the version alone. Before installing a backport, check that its changelog names both CVEs.

For Apache, list the sites and how each one runs PHP:

Terminal window
sudo apache2ctl -S
sudo apache2ctl -M | grep -E 'php|mpm|proxy|fcgi'
sudo grep -RInE 'ServerName|DocumentRoot|SetHandler|ProxyPassMatch' /etc/apache2/sites-enabled

A site with SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost" runs on that FPM pool. A site without one runs on Apache’s mod_php, if mod_php is loaded.

For Nginx, match each site’s fastcgi_pass to the FPM pool that listens on it:

Terminal window
sudo grep -RInE 'server_name|fastcgi_pass' /etc/nginx/sites-enabled
sudo grep -RInE '^[[:space:]]*listen[[:space:]]*=' /etc/php/*/fpm/pool.d

Check .htaccess and .user.ini files too: they can switch a site’s handler or settings.

Then save the configuration and the package list:

Terminal window
backup=$(sudo mktemp -d /var/backups/php-security.XXXXXX)
for dir in /etc/php /etc/apache2 /etc/nginx; do
if [ -d "$dir" ]; then sudo cp -a "$dir" "$backup/"; fi
done
dpkg-query -W | sudo tee "$backup/packages-before.txt" >/dev/null
echo "Backup saved in $backup"

The later steps use $backup, so keep this shell open, or set backup= to the printed path again.

Pick your path

Your server Path What I patched there
CloudPanel on Ubuntu 24.04 1. CloudPanel 5.6, 7.2, 7.3, 7.4, 8.0 and 8.2 to 8.5
Ubuntu 22.04 with Ondřej Surý’s PPA 2. Ondřej Surý’s PPA 7.4 and 8.3
Ubuntu 24.04 with Ubuntu’s own PHP 3. Ubuntu packages 8.3: Ubuntu’s fix was already in; later moved to 8.3.36 from the PPA
Ubuntu 20.04 with PHP 7.4 4. Ubuntu Pro ESM 7.4

The steps work for any branch the path supports: the last column is only what my servers ran.

Every path ends with the same check: ask the website.

Path 1: CloudPanel on Ubuntu 24.04

CloudPanel installs PHP 7.1 to 8.5 from its own repository, and its installer pins every package from that repository at priority 1000. It does not add Ondřej Surý’s PPA. On my host the panel update fixed the current branches, but the older ones only had fixed builds in the PPA, so they needed a provider switch.

On my host: PHP 5.6, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 and 8.5. clp-update fixed 8.2 to 8.5, 5.6 was already fixed from the PPA, and I switched 7.2, 7.3, 7.4 and 8.0 to the PPA. 7.1 and 8.1 were installed, but no site I mapped used them, so I left them for later.

Step 1: Run the panel update

Terminal window
sudo clp-update

This updates the whole server, not just PHP. On my host it proposed 328 upgrades and 35 new packages, including the kernel, Nginx, SSH, the database and cache services, and CloudPanel itself. Read the list before you confirm, and do not interrupt APT once it starts installing. A new kernel needs a reboot, which step 10 covers.

Then check that nothing broke, and see where every installed branch now comes from:

Terminal window
sudo dpkg --audit
systemctl --failed
apt-cache policy $(dpkg-query -W -f='${binary:Package}\n' 'php*-fpm' 2>/dev/null | grep -E '^php[0-9.]+-fpm$')

For each branch, the line marked *** is the installed version, and the line under it names its repository. Then ask the older branches’ changelogs:

Terminal window
for b in 7.1 7.2 7.3 7.4 8.0 8.1; do
printf 'PHP %s: ' "$b"
zgrep -cE 'CVE-2026-10961[89]' "/usr/share/doc/php$b-common/changelog.Debian.gz"
done

CloudPanel’s 7.1 to 8.1 builds printed 0: their newest changelog entries were from July 2025. On my host the branches fell into three groups:

Branches After clp-update What to do
8.2 to 8.5 CloudPanel’s own fixed builds Only restart the workers, in step 9
5.6 Surý’s PPA, already the fixed -104 build Upgrade it from the PPA, in step 3
7.1 to 8.1 CloudPanel’s old, unfixed builds Switch them to the PPA, steps 4 to 8

To see which sites use which branch, compare the fastcgi_pass lines with the pools, as in Before you start. CloudPanel’s pools listen on local ports named after the branch: 127.0.0.1:14001 and up for PHP 7.4, 15001 and up for 8.0, and so on.

Step 2: Add Ondřej Surý’s PPA, if it is not there yet

Why I had it already: CloudPanel does not ship PHP 5.6, its oldest branch is 7.1, and one application on this server still needs 5.6. I added Surý’s PPA to install php5.6-fpm for that site. That is also why this host had fixed builds within reach for the older branches.

A stock CloudPanel server has no Surý packages at all. Check whether the PPA is set up:

Terminal window
grep -rl 'ondrej/php' /etc/apt/sources.list.d/ || echo 'PPA not added'

If it is not, add it the way its Launchpad page (opens in a new tab) describes:

Terminal window
sudo apt-get install software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt-get update

The PPA builds for Ubuntu 22.04 and 24.04. For newer releases, it points to packages.sury.org (opens in a new tab) instead.

Adding the PPA does not touch CloudPanel’s PHP yet: CloudPanel’s pin at 1000 still beats the PPA’s default 500, which step 4 shows. It does bring newer builds of some shared libraries, though, so simulate the next upgrade (sudo apt-get -s upgrade) before you run one.

Step 3: Upgrade the branches that already come from the PPA

CloudPanel has no package of its own for PHP 5.6, so its pin does not get in the way: a normal upgrade from the PPA is enough. The commands below use b=5.6. For any other branch that already comes from the PPA, change only that line.

Terminal window
b=5.6
apt-cache policy "php$b-fpm"
mapfile -t packages < <(
dpkg-query -W -f='${binary:Package} ${db:Status-Status}\n' |
awk -v b="$b" '$2 == "installed" && ($1 == "php" b || index($1, "php" b "-") == 1) { print $1 }'
)
sudo apt-get -s --no-remove --only-upgrade install "${packages[@]}"
sudo apt-get --no-remove --only-upgrade install "${packages[@]}"
zgrep -cE 'CVE-2026-10961[89]' "/usr/share/doc/php$b-common/changelog.Debian.gz"

The last line should print 1 or more. The 5.6 entry reads “Backported from 8.2.35: [CVE-2026-109618, CVE-2026-109619]: Avoid state corruption via bailout”. My 5.6 was already the fixed 5.6.40-104, so the simulation had nothing to install; a newer revision such as -105 has the fix too. Its FPM service is restarted with the others in step 9.

Step 4: Pick a branch and see why it did not move

Steps 4 to 8 switch one branch from CloudPanel’s build to the PPA’s. They are the same for 7.1, 7.2, 7.3, 7.4, 8.0 and 8.1. The only thing to change is the b= line: every command after it uses $b. Run steps 4 to 8 for one branch, test its sites, then start again here with the next. I started with the branch that served the fewest sites.

Terminal window
b=7.4
apt-cache policy "php$b-fpm"
sudo grep -RInE 'Package:|Pin:|Pin-Priority:' /etc/apt/preferences.d

CloudPanel pins its own repository at priority 1000 and the PPA sits at 500, so APT keeps choosing CloudPanel’s old package even though the PPA has a fixed one.

Step 5: Pin the branch to the PPA, and check the new build first

A narrow pin for this one branch makes the PPA’s build the candidate and leaves CloudPanel’s global pin alone. Priority 1001 lets APT replace a package even when CloudPanel’s version string sorts higher, and APT may call some of these switches downgrades for that reason:

Terminal window
sudo tee "/etc/apt/preferences.d/php$b-sury.pref" >/dev/null <<EOF
Package: php$b php$b-*
Pin: origin ppa.launchpadcontent.net
Pin-Priority: 1001
EOF
apt-cache policy "php$b-fpm"

The file name must end in .pref: APT silently ignores other names in preferences.d, and php7.4-sury alone would count as the extension .4-sury. The candidate should now be the PPA’s build. Nothing is installed yet.

Before installing, download the candidate into a folder of your own, check that its changelog names the fixes, and test its FPM binary against the pools CloudPanel already has. No sudo is needed until the last line:

Terminal window
mkdir -p ~/php-package-review && cd ~/php-package-review
rm -rf "php$b-common_"*.deb "php$b-fpm_"*.deb "candidate-$b"
apt-get download "php$b-common" "php$b-fpm"
for deb in "php$b-common_"*.deb "php$b-fpm_"*.deb; do dpkg-deb -x "$deb" "candidate-$b"; done
zgrep -cE 'CVE-2026-10961[89]' "candidate-$b/usr/share/doc/php$b-common/changelog.Debian.gz"
sudo "candidate-$b/usr/sbin/php-fpm$b" -t --fpm-config "/etc/php/$b/fpm/php-fpm.conf"

The count should be 1 or more, and the test should end with test is successful. These are the fixed builds I installed:

Branch PPA build
7.1 7.1.33-80+ubuntu24.04.1+deb.sury.org+1
7.2 7.2.34-66+ubuntu24.04.1+deb.sury.org+1
7.3 7.3.33-35+ubuntu24.04.1+deb.sury.org+1
7.4 1:7.4.33-31+ubuntu24.04.1+deb.sury.org+1
8.0 1:8.0.30-25+ubuntu24.04.1+deb.sury.org+1
8.1 8.1.34-9+ubuntu24.04.1+deb.sury.org+1

Step 6: Note the extensions CloudPanel loads by hand

CloudPanel’s php.ini files load some extensions directly, with extension= lines near the end. Surý’s packages load extensions from conf.d instead, and the switch does not bring CloudPanel’s along. List them before the switch:

Terminal window
sudo grep -nE '^[[:space:]]*(zend_)?extension[[:space:]]*=' "/etc/php/$b/cli/php.ini" "/etc/php/$b/fpm/php.ini"

On my host 7.2, 7.3 and 7.4 loaded mcrypt, memcached, redis and imagick; 8.0 loaded the same without mcrypt.

Step 7: Switch the branch, with its extensions

Simulate the switch for every installed package of the branch, plus the PPA’s packages for the extensions from step 6, then run it. Change the extension list to match what step 6 printed:

Terminal window
mapfile -t packages < <(
dpkg-query -W -f='${binary:Package} ${db:Status-Status}\n' |
awk -v b="$b" '$2 == "installed" && ($1 == "php" b || index($1, "php" b "-") == 1) { print $1 }'
)
extensions=("php$b-mcrypt" "php$b-memcached" "php$b-redis" "php$b-imagick")
sudo apt-get -s --no-remove install "${packages[@]}" "${extensions[@]}"
sudo apt-get --no-remove -o Dpkg::Options::='--force-confold' install "${packages[@]}" "${extensions[@]}"

--no-remove makes APT stop rather than remove anything. On my host each switch replaced about 20 packages, added the extensions plus igbinary and msgpack, which memcached and redis need, and removed nothing.

Step 8: Restore the settings, then restart

The switch replaced CloudPanel’s php.ini files, even with --force-confold. The new ones lost the upload limits, memory limit, short tags, session lifetime and timezone. Put the backed-up files back. The security fix is in the binaries, so restoring the configuration keeps it:

Terminal window
sudo cp -a "$backup/php/$b/cli/php.ini" "/etc/php/$b/cli/php.ini"
sudo cp -a "$backup/php/$b/fpm/php.ini" "/etc/php/$b/fpm/php.ini"
sudo cp -a "$backup/php/$b/fpm/php-fpm.conf" "/etc/php/$b/fpm/php-fpm.conf"

The restored files still load the extensions by hand, and conf.d now loads them too. Check that conf.d has them, then comment out only the duplicate lines in php.ini:

Terminal window
grep -H '^extension=' "/etc/php/$b/cli/conf.d/"*.ini "/etc/php/$b/fpm/conf.d/"*.ini |
grep -E 'mcrypt|memcached|redis|imagick'
for ini in "/etc/php/$b/cli/php.ini" "/etc/php/$b/fpm/php.ini"; do
sudo sed -i -E \
'/^[[:space:]]*extension[[:space:]]*=[[:space:]]*(mcrypt|memcached|redis|imagick)\.so[[:space:]]*$/s/^/; Loaded through conf.d: /' \
"$ini"
done

Left in, the duplicates produce Module 'imagick' already loaded warnings, and memcached and redis fail with undefined symbol: php_json_decode_ex, because php.ini loads them before the JSON module. Then check, restart and look at the result:

Terminal window
"php$b" -m
sudo "php-fpm$b" -t
sudo systemctl daemon-reload
sudo systemctl restart "php$b-fpm"
systemctl is-active "php$b-fpm" nginx
sudo ss -ltnp | grep "php-fpm$b"
sudo journalctl -u "php$b-fpm" --since '2 minutes ago' --no-pager
sudo apt-get -s upgrade | awk -v b="$b" '$1 == "Inst" && ($2 == "php" b || index($2, "php" b "-") == 1)'

php -m should list the extensions with no warnings above it. daemon-reload matters because the PPA’s systemd unit differs from CloudPanel’s: it restarts only on failure, and adds a socket helper. ss should show the pool’s ports listening again. The last command simulates the next upgrade. Any line it prints must name the PPA (PPA for PHP) as the source: a line that names CloudPanel’s repository means the branch would swap back, so check the pin.

Test the branch’s sites, then go back to step 4 and change b= to the next branch. I switched 7.2, 7.3, 7.4 and 8.0 this way; 7.1 and 8.1 take exactly the same steps. If no site uses a branch, removing it is simpler still, once you have checked that nothing points at its FPM pool.

Step 9: Restart every PHP-FPM service

Restart every running PHP-FPM service, so none keeps workers from before the fixes. That covers 8.2 to 8.5 from clp-update and 5.6 from step 3:

Terminal window
systemctl list-units --type=service --state=running --plain --no-legend 'php*-fpm.service' |
awk '{ print $1 }' | xargs -r sudo systemctl restart
systemctl list-units --type=service --plain --no-legend 'php*-fpm.service'
sudo nginx -t
sudo dpkg --audit

Every PHP-FPM unit should show active running, and nginx -t should succeed.

Step 10: Reboot for the new kernel

clp-update installed a new kernel that only runs after a reboot. The reboot interrupts every site and your SSH session, so do it in the window, with console access at hand:

Terminal window
sudo reboot

Once the server is back:

Terminal window
uname -r
systemctl --failed
systemctl list-units --type=service --plain --no-legend 'nginx.service' 'mysql.service' 'php*-fpm.service'

uname -r should print the new kernel, systemctl --failed should list nothing, and every service should be active. Then ask the website which build it runs.

Path 2: Ubuntu 22.04 with Ondřej Surý’s PPA

On my host: PHP 7.4 from 7.4.33-30 to 7.4.33-31, and PHP 8.3 from 8.3.33 to 8.3.36, both from the PPA. Apache served every site through PHP-FPM: most on the 7.4 pool, a few on 8.3.

This host already used the PPA, so the fixed builds were one refresh away. To add the PPA first, see Path 1, step 2. On a server that runs Ubuntu’s own PHP, adding it switches providers, so Path 3 is the smaller change.

Step 1: Refresh and check the candidates

Terminal window
sudo apt-get update
apt-cache policy php7.4-fpm php8.3-fpm

Before the refresh, APT still offered 8.3.35 and no new 7.4. After it, the candidates were the fixed 7.4.33-31 and 8.3.36. Check their changelogs inside the packages, as in Path 1, step 5, with b=7.4 and then b=8.3: apt-get changelog could not fetch them. The 7.4 entry names both CVEs, backported from 8.2.35.

Step 2: Simulate the upgrade of every installed package of those branches

Terminal window
mapfile -t packages < <(
dpkg-query -W -f='${binary:Package} ${db:Status-Status}\n' |
awk '$2 == "installed" && $1 ~ /^(php(7[.]4|8[.]3)($|-)|libapache2-mod-php(7[.]4|8[.]3)$)/ { print $1 }'
)
sudo apt-get -s --no-remove --only-upgrade install "${packages[@]}"

Change 7[.]4|8[.]3 to the branches you run. Upgrading every package of a branch together keeps its extensions on the same build. Mine showed 28 upgrades, no new packages and no removals.

Step 3: Install, then test the configuration

Terminal window
sudo php-fpm7.4 -t && sudo php-fpm8.3 -t && sudo apache2ctl configtest
sudo apt-get --no-remove --only-upgrade -o Dpkg::Options::='--force-confold' install "${packages[@]}"
sudo diff -qr "$backup/php" /etc/php
sudo php-fpm7.4 -t && sudo php-fpm8.3 -t && sudo apache2ctl configtest

The host also had the libapache2-mod-php packages installed but not enabled, so the install printed notices that suggest a2enmod and a2enconf. Do not run them when the sites already use FPM: they would switch Apache to mod_php.

Step 4: Restart the workers

The new code only runs in new workers:

Terminal window
sudo systemctl restart php7.4-fpm php8.3-fpm
systemctl is-active php7.4-fpm php8.3-fpm apache2
dpkg-query -W -f='${Package}\t${Version}\n' php7.4-fpm php8.3-fpm
sudo dpkg --audit

If Apache’s mod_php serves any site, restart apache2 too.

Path 3: Ubuntu 24.04 with Ubuntu’s own PHP

On my host: PHP 8.3 on Apache’s mod_php, already at the fixed 8.3.6-0ubuntu0.24.04.13. I restarted Apache to be sure it ran the fix, and later moved the host to upstream 8.3.36 from Surý’s PPA.

Ubuntu backports the fix into its own 8.3.6 package, so you do not need a PPA to be safe. Ubuntu’s security tracker rates both CVEs Critical and lists 8.3.6-0ubuntu0.24.04.13 as the fixed revision for 24.04: CVE-2026-109618 (opens in a new tab), CVE-2026-109619 (opens in a new tab).

Step 1: Check the revision and its changelog

Terminal window
sudo apt-get update
apt-cache policy php8.3-common libapache2-mod-php8.3
zgrep -i -C 5 -E 'CVE-2026-10961[89]' /usr/share/doc/php8.3-common/changelog.Debian.gz

Ubuntu’s entry starts with “SECURITY UPDATE: Remote RCE via state corruption issue” and names both CVEs. If the installed revision is older than 8.3.6-0ubuntu0.24.04.13, upgrade the installed php8.3 packages with the same steps as Path 2, using 8[.]3 as the branch.

Step 2: Find out when it arrived, and restart

The fixed package was already installed before I looked. The package log shows when:

Terminal window
sudo zgrep -hE '(install|upgrade) (libapache2-mod-php8\.3|php8\.3-common)(:amd64)? ' /var/log/dpkg.log*

On my host the .13 revision arrived on October 22, the day after the release. That log does not show whether Apache restarted afterwards, and mod_php runs inside Apache’s workers, so restart it:

Terminal window
sudo apache2ctl configtest && sudo systemctl restart apache2
systemctl is-active apache2

If the site uses PHP-FPM, restart php8.3-fpm instead.

Step 3 (optional): Move to upstream 8.3.36

Ubuntu’s 8.3.6 with backports is safe. I still moved this host to Surý’s 8.3.36, which tracks upstream PHP. This switches who maintains the server’s PHP, so it is a choice, not part of the fix. Add the PPA as in Path 1, step 2, then simulate and install every PHP 8.3 package together:

Terminal window
mapfile -t packages < <(
dpkg-query -W -f='${binary:Package} ${db:Status-Status}
' |
awk '$2 == "installed" && $1 ~ /^(php8[.]3($|-)|libapache2-mod-php8[.]3$)/ { print $1 }'
)
sudo apt-get -s --no-remove --only-upgrade install "${packages[@]}"
sudo apt-get --no-remove --only-upgrade -o Dpkg::Options::='--force-confold' install "${packages[@]}"
sudo diff -qr "$backup/php/8.3" /etc/php/8.3
sudo apache2ctl configtest && sudo systemctl restart apache2

All 13 packages moved to 8.3.36, with no new packages or removals, and /etc/php/8.3 matched the backup. The package also replaced /etc/apache2/mods-available/php8.3.conf; it still hands .php files to mod_php.

Path 4: Ubuntu 20.04 with Ubuntu Pro (ESM)

On my host: PHP 7.4, to the fixed 7.4.3-4ubuntu2.29+esm5 build, served by both mod_php and FPM.

Ubuntu 20.04 is out of standard support, so its PHP 7.4 fix (7.4.3-4ubuntu2.29+esm5, the revision Ubuntu’s tracker (opens in a new tab) lists for 20.04) only comes through Ubuntu Pro’s Expanded Security Maintenance. Without it, APT offered nothing newer than 7.4.3-4ubuntu2.29, whose changelog does not name the fixes. Surý’s free PPA does not build for 20.04: do not point 20.04 at the 22.04 or 24.04 repositories to get newer PHP.

Step 1: Get an Ubuntu Pro subscription

Ubuntu Pro is free for personal use (opens in a new tab) on up to 5 machines, or 50 for official Ubuntu Community members. Sign in at ubuntu.com/pro (opens in a new tab) with an Ubuntu One account to get it. Servers that run a business or a client’s sites need a paid subscription.

Step 2: Update the Pro client, attach the server and turn on ESM

My host had an old Pro client (28.1). Update it first; this changes no PHP package:

Terminal window
sudo apt-get update
sudo apt-get --no-remove install --only-upgrade ubuntu-advantage-tools
sudo pro status

On my host pro status then failed with “Cannot verify certificate of server”, because a custom OpenSSL in /usr/local looked for CA certificates in /usr/local/ssl, where there were none. curl worked, so the network was fine. Pointing each pro command at the system CA bundle fixed it, with verification still on:

Terminal window
sudo env SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt pro status

If pro status works without it on your server, drop the env SSL_CERT_FILE=… part from the commands below. To attach, copy your token from the Ubuntu Pro dashboard (opens in a new tab) and replace TOKEN:

Terminal window
sudo env SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt pro attach TOKEN

The token is a secret: anyone who has it can attach machines to your subscription. Keep it out of scripts, tickets and shared logs, and clear it from the shell history afterwards (history -d with the line number, or start the command with a space if HISTCONTROL includes ignorespace). If it ever ends up somewhere shared, reset it in the dashboard. Each attached server uses one of your machines, and sudo pro detach gives it back.

Attaching enabled ESM Apps and ESM Infra. It also tried Livepatch and reported “Failed to enable default services”; Livepatch is separate from PHP updates, so that can wait. Check the status and refresh APT:

Terminal window
sudo env SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt pro status
sudo apt-get update
apt-cache policy php7.4-common php7.4-fpm libapache2-mod-php7.4

esm-infra and esm-apps should show enabled, and the candidates should be 7.4.3-4ubuntu2.29+esm5 from esm.ubuntu.com.

Step 3: Check the ESM build, then upgrade

apt-get changelog cannot fetch ESM changelogs, and downloading as a normal user fails with 401 Unauthorized: the ESM credentials are readable by root only. Download with sudo:

Terminal window
mkdir -p ~/php74-security-check && cd ~/php74-security-check
sudo apt-get download php7.4-common=7.4.3-4ubuntu2.29+esm5
sudo dpkg-deb -x php7.4-common_7.4.3-4ubuntu2.29+esm5_amd64.deb candidate
zgrep -i -C 5 -E 'CVE-2026-10961[89]' candidate/usr/share/doc/php7.4-common/changelog.Debian.gz

APT warns that the download ran “unsandboxed as root”, because the _apt user cannot write to your home folder. That is harmless. The changelog should show debian/patches/CVE-2026-109618_109619.patch. Then simulate and install every PHP 7.4 package together, as in Path 2 with 7[.]4 as the branch:

Terminal window
mapfile -t packages < <(
dpkg-query -W -f='${binary:Package} ${db:Status-Status}\n' |
awk '$2 == "installed" && $1 ~ /^(php7[.]4($|-)|libapache2-mod-php7[.]4$)/ { print $1 }'
)
sudo apt-get -s --no-remove --only-upgrade install "${packages[@]}"
sudo apt-get --no-remove --only-upgrade -o Dpkg::Options::='--force-confold' install "${packages[@]}"
sudo diff -qr "$backup/php/7.4" /etc/php/7.4

On my host this upgraded 19 packages to +esm5, including mod_php, FPM, the CLI and the packaged extensions. It added and removed nothing, and the php.ini files and Imagick were untouched. pro can also confirm the result: sudo pro fix CVE-2026-109618 --dry-run reports whether the CVE still affects the server, without changing anything.

Step 4: Restart both kinds of worker

This host served PHP through both mod_php and FPM, so both needed new workers:

Terminal window
sudo php-fpm7.4 -t &&
sudo apache2ctl configtest &&
sudo systemctl restart php7.4-fpm apache2
systemctl is-active apache2 php7.4-fpm
sudo dpkg --audit

The probe below then showed 7.4.3-4ubuntu2.29+esm5 on both a mod_php site (apache2handler) and an FPM site (fpm-fcgi). No OS upgrade or custom build was needed.

Without Ubuntu Pro, the options are a paid PHP LTS such as Freexian’s (opens in a new tab), a PHP 7.4 build from PHP’s own source backport (opens in a new tab), or moving the server to 22.04 or 24.04. I also built PHP 8.3 from source on this host, but only to move sites off 7.4 later: that is a separate note, not part of the fix.

How do I prove the website runs the fixed PHP?

A fixed package and a running service do not prove which PHP answers the website. This script drops a tiny probe into the site’s document root, asks the site for it over local HTTPS, and deletes it again, even if a step fails:

Terminal window
(
set -e
root=/var/www/example.com/public
host=example.com
probe=$(sudo mktemp "$root/php-runtime-XXXXXXXX.php")
trap 'sudo rm -f -- "$probe"' EXIT
sudo tee "$probe" >/dev/null <<'PHP'
<?php
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
if (!in_array($ip, array('127.0.0.1', '::1'), true)) {
http_response_code(404);
exit;
}
header('Content-Type: text/plain');
header('Cache-Control: no-store');
echo 'PHP: ', PHP_VERSION, "\n";
echo 'Handler: ', PHP_SAPI, "\n";
echo 'INI: ', php_ini_loaded_file(), "\n";
echo 'Upload progress: ', ini_get('session.upload_progress.enabled') ? 'on' : 'off', "\n";
PHP
sudo chmod 644 "$probe"
curl --fail --silent --show-error --max-time 20 --noproxy '*' \
--resolve "$host:443:127.0.0.1" \
"https://$host/$(basename "$probe")"
)

Set root and host for each site. The probe answers only requests from the server itself and returns 404 to everyone else, so opening it in your own browser shows a 404 too. It is written to run on PHP 5.6 as well. Handler reads apache2handler for mod_php and fpm-fcgi for PHP-FPM.

Do not check with a public phpinfo() page instead. It shows the server’s whole configuration to anyone who finds the URL, for as long as you forget to delete it. Two caveats for the probe:

  • If a reverse proxy on the same machine forwards public traffic, the public requests also arrive from 127.0.0.1. The probe only lives for a second, but know that it is reachable during that second.
  • Prepend files, such as Wordfence’s firewall, run before the probe.

PHP_VERSION shows the base version (8.3.6 on Ubuntu, 7.4.3-4ubuntu2.29+esm5 on ESM), so compare it with the package revision you installed. Then test the parts of the application that matter: logging in, pages that read the database, and uploads.

How do I lower the risk before I can patch?

These lower the risk while you prepare the update. None of them replaces it.

  • Turn off session upload progress in the startup configuration, then restart every worker. On October 23, PHP published a follow-up advisory (CVE-2026-115560) about an unlikely remote code execution path through it, and suggests the same until the next regular release:

    session.upload_progress.enabled = 0

    Set it in each branch’s php.ini, or per FPM pool with php_admin_flag[session.upload_progress.enabled] = off. ini_set() in application code runs too late. Applications that never change the session serialize handler at runtime can keep the feature on. The probe above prints its current state.

  • Limit what requests can make PHP allocate, at the web server or proxy: body size, upload size, multipart headers and input nesting. Block multipart requests on paths that do not need them.

  • Be careful with memory_limit = -1. PHP’s advisory for CVE-2026-109618 (opens in a new tab) suggests it to remove one predictable trigger, but it also removes the per-request memory cap, so it needs memory limits on the workers or containers instead. PHP advises against RLIMIT_AS and RLIMIT_DATA for this.

Nginx, October 14

Nginx fixed five issues on October 14: CVE-2026-15176, CVE-2026-93479, CVE-2026-95101, CVE-2026-97629 and CVE-2026-97205. The fixed releases are 1.30.6 (stable) and 1.31.7 (mainline). Which configurations are affected differs per issue, so read Nginx’s advisories (opens in a new tab). The same day, njs 1.0.2 fixed CVE-2026-101025 and CVE-2026-101019, for servers that use Nginx’s JavaScript module.

nginx -v shows the binary on disk. Check the package, then ask the running master process which binary it started from:

Terminal window
nginx -v
dpkg-query -W -f='${Package}\t${Version}\n' 'nginx*' 'libnginx-mod-*'
pid=$(systemctl show nginx -p MainPID --value)
if [ "$pid" -gt 0 ]; then
sudo "/proc/$pid/exe" -v
sudo readlink "/proc/$pid/exe"
fi

My CloudPanel host had 1.30.6-1+clp-noble for Nginx and every module, and the running master reported nginx/1.30.6 from /usr/sbin/nginx, with no (deleted) after the path. If you see (deleted), the package was upgraded but Nginx was never restarted. This check does not cover njs or third-party modules.

Sources


Next in Security & Identity