← posts / infrastructure

PHP2Shell on your own server: reaching PHP 8.2.35+ on php-fpm, mod_php and FrankenPHP, and the stopgap when you're stuck on 8.1

php.net's 21 October release fixes two unauthenticated RCEs that hit every PHP web SAPI. Version checks that prove the running worker is patched, upgrade paths for distro packages, Docker images and FrankenPHP, and php.net's interim mitigations for 8.1 and older.

if.codesOct 25, 2026 · 5 min read#php#security#self-hosting#dockerAI-assisted

On 21 October 2026 php.net shipped PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. All four fix the same two bugs, CVE-2026-109618 and CVE-2026-109619, both rated CVSS v4.0 9.2 and both reachable by an unauthenticated remote request. The advisory lists every PHP installation that serves web requests as affected: PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. It calls mod_php the highest-risk setup, because a demonstrated exploit fires before any application code runs. Your framework, WAF rules in PHP and login checks never get a say.

This guide gets a self-hosted box or container to a fixed build and, more importantly, proves that the process answering HTTP is the fixed one. It should take about 15 minutes per server.

What the two bugs are

  • CVE-2026-109618 (RCE via OOM corruption). Out-of-memory handling can interrupt PHP while internal request data is only partly initialised; request cleanup then treats that data as valid.
  • CVE-2026-109619 (RCE via stale global state). Request-owned state can outlive the memory behind it, and a later request on the same worker can use it before normal initialisation resets it.

Both are fixed in exactly the four releases above. There is no fixed 8.1 or older from php.net.

A conceptual representation of a critical security vulnerability in a web server.
A conceptual representation of a critical security vulnerability in a web server.

Step 1: find the version the web worker actually runs

The trap is that php -v reports the CLI binary. FPM, mod_php and FrankenPHP can be a different build, and a running FPM master keeps the old code in memory until it is restarted. Check what the web server answers with. Drop a throwaway file into the document root, request it, then delete it:

echo '<?php echo PHP_VERSION, " ", PHP_SAPI, "\n";' | sudo tee /var/www/html/ver-7f3a.php
curl -s https://your-site.example/ver-7f3a.php
sudo rm /var/www/html/ver-7f3a.php

Typical output:

8.3.30 fpm-fcgi

The second word tells you the SAPI: fpm-fcgi, apache2handler (mod_php), cgi-fcgi or frankenphp. Then compare the version against the fixed release for its branch. This script does the comparison and exits non-zero when the build is not fixed, so you can loop it over hosts:

#!/usr/bin/env bash
# usage: ./php-patched.sh 8.3.30
v="${1:?usage: php-patched.sh <version>}"
case "${v%.*}" in
  8.5) min=8.5.12 ;;
  8.4) min=8.4.27 ;;
  8.3) min=8.3.36 ;;
  8.2) min=8.2.35 ;;
  *) echo "$v: no php.net fix on this branch, upgrade or use a vendor backport"; exit 2 ;;
esac
if [ "$(printf '%s\n%s\n' "$min" "$v" | sort -V | head -n1)" = "$min" ]; then
  echo "$v: fixed (>= $min)"
else
  echo "$v: VULNERABLE (needs $min or later)"
  exit 1
fi

One caveat: distributions that backport security fixes often keep the old upstream version number. A Debian or Ubuntu package can say 8.2.x with a lower patch number and still carry the fix. For distro packages, check the changelog for the CVE IDs instead of trusting the number:

apt changelog php8.2-fpm 2>/dev/null | grep -m3 -E 'CVE-2026-10961(8|9)'

No match means the package you have does not mention the fix yet.

Step 2: upgrade, by how PHP got onto the box

Distro or PPA packages (php-fpm or mod_php)

Upgrade only the PHP packages, then restart the service that holds the interpreter. On Debian or Ubuntu with an FPM pool for 8.3:

A server environment where PHP-FPM or mod_php processes are running.
A server environment where PHP-FPM or mod_php processes are running.
sudo apt-get update
sudo apt-get install --only-upgrade 'php8.3*'
sudo systemctl restart php8.3-fpm

For mod_php the interpreter lives inside Apache, so restart Apache rather than reload it:

sudo systemctl restart apache2

Whether your distribution has published a fixed package yet is up to its security team. If apt offers nothing new, check the distro's security tracker for the two CVE IDs before assuming you are covered.

Official Docker images

Floating tags like php:8.3-fpm only move when you pull, and a cached base layer can quietly keep the old build. Pin the fixed patch release and rebuild without the cache:

FROM php:8.3.36-fpm
docker build --pull --no-cache -t myapp:php-8.3.36 .
docker run --rm myapp:php-8.3.36 php -r 'echo PHP_VERSION, "\n";'

Then redeploy and re-run the Step 1 check against the live site, not the image.

FrankenPHP

FrankenPHP embeds libphp, so the PHP fix arrives only with a FrankenPHP build that links a fixed PHP. Upgrading a system PHP package next to it changes nothing. Print what the binary carries:

frankenphp version

If the PHP version it reports is below the fixed release for its branch, update the FrankenPHP binary or image and restart it. The Step 1 file works here too and reports frankenphp as the SAPI.

Need the backend underneath done properly?I build and harden Go and Python backends. The estimate is free.

Step 3: prove the old workers are gone

An FPM master started before the upgrade still serves the old code. Compare its start time with the moment you upgraded:

ps -o pid,lstart,cmd -C php-fpm8.3 | head -n 3

If the master started before your apt-get run, restart it. Then repeat the Step 1 request: that answer, not the package manager, is your proof.

Stuck on 8.1 or older

php.net no longer ships updates for 8.1 and older. Your options, in order:

  1. Get a fixed build from your distribution or an LTS vendor, if you pay for one.
  2. Build from source with the minimal backports the PHP security team published for 7.4, 8.0 and 8.1, on branches named PHP-<version>-CVE-2026-109618-109619 in php-src.
  3. Apply the interim mitigations below while you migrate. php.net is explicit that none of them make an installation safe; updating is the only complete fix.

Interim mitigations from the advisory

For CVE-2026-109618 (OOM):

A conceptual image representing temporary mitigations while awaiting a full upgrade.
A conceptual image representing temporary mitigations while awaiting a full upgrade.
  • Set memory_limit=-1, ideally with php_admin_value in the FPM pool or Apache config, and make sure nothing restores a finite limit. Pair it with a container or cgroup memory limit; the advisory says not to use RLIMIT_AS or RLIMIT_DATA.
  • Set post_max_size = 8M if you take no uploads, or 32M if you do, with upload_max_filesize below it.
  • Enforce limits at the web server or proxy on decoded request size, field count, multipart part count, header count and nesting depth.

For CVE-2026-109619 (stale state):

  • Set session.upload_progress.enabled=0 in php.ini, the FPM pool or server config, then restart every worker. ini_set() and .user.ini are too late.
  • On 8.5, set fatal_error_backtraces=0 and restart.
  • Remove invalid or deprecated per-request settings that emit diagnostics before initialisation, and fix fatal errors and timeouts an attacker can trigger. Hiding errors with display_errors or @ does not help.

As an FPM pool fragment:

; /etc/php/8.1/fpm/pool.d/www.conf  (stopgap only, not a fix)
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] = off

Note that 8.2 itself reaches end of life on 31 December 2026, so a box you bring to 8.2.35 today needs another move within weeks.

Sources

if.codesI build RAG, AI integrations and agent pipelines on Go and Python backends — and write about it here.
// keep reading

More posts on AI and backends.

// free quote

Read something you need? I’ll quote it for free.

RAG, AI integrations, agents or the backend underneath — tell me what you have and what should change. I read every request myself.

Free · no commitment

Tell me what you have. I’ll tell you what it takes.

1Describe the projectA few sentences is enough — about two minutes.
2I review itI read it myself and may ask a follow-up question.
3You get a free quoteScope, approach and estimate — yours to keep, no strings.
What kind of project is it?
Free and without obligation. Your details are used only to reply — see the privacy policy.