# How to Update PHP After PHP2Shell: A Practical Guide

**Author:** Oleksii Turovskyi, Full-Stack Developer, AI & Enterprise Architecture
**Published:** 2026-10-27 · **Updated:** 2026-10-27
**Canonical:** https://mettevo.com/guides/update-php-after-php2shell

---

If your servers run PHP from 5.4 to 8.5.11, update them now: PHP2Shell (CVE-2026-109618 and CVE-2026-109619) lets an unauthenticated attacker run code on the server. PHP rates both 9.2 on CVSS v4.0. Amazon logged the first exploitation attempt on 17 October 2026, four days before the fix, and CISA added both CVEs to its Known Exploited Vulnerabilities catalog on 26 October, with a remediation deadline of 29 October for US federal agencies.

<figure>
  <img
    src="/blog-assets/php2shell-update-hero.webp"
    srcset="/blog-assets/php2shell-update-hero-768.webp 768w, /blog-assets/php2shell-update-hero.webp 1536w"
    sizes="(max-width: 860px) 100vw, 820px"
    alt="Title card for the article How to Update PHP After PHP2Shell: A Practical Guide. On the right, a code window shows the lines &lt;?php and php --version next to a large PHP logo with a refresh icon and a green shield with a check mark. Below the title, a red warning triangle points through a row of arrows towards the shield."
    width="1536"
    height="1024"
    fetchpriority="high"
  />
  <figcaption>
    From "probably vulnerable" to "patched, restarted and verified".
  </figcaption>
</figure>

The bugs sit in PHP itself, in how it handles memory and internal state while it starts and cleans up each request, so updating WordPress, Laravel or your Composer packages does not help. Every way of serving PHP is affected: PHP-FPM behind nginx, Apache or Caddy, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed. PHP rates mod_php as the highest risk.

This guide takes a Debian or Ubuntu server from "probably vulnerable" to "patched, restarted and verified" in six steps, and says what to do when your release gets no fix at all.

## Step 1: find out what you actually run

Judge a server by its package revision, not by `php -v`. Ubuntu and Debian keep the upstream version and fix it in the package revision: a fully patched Ubuntu 24.04 still reports `PHP 8.3.6`, far below the upstream fix 8.3.36.

List every PHP package with its exact build, and see which ones serve requests:

```bash
# Every PHP package and its exact build
dpkg-query -W -f='${binary:Package} ${Version}\n' 'php*' 'libapache2-mod-php*' | sort

# PHP-FPM pools that run, and whether Apache loads mod_php
systemctl list-units --type=service --state=running 'php*-fpm*'
apache2ctl -M 2>/dev/null | grep -i php

# Where a build comes from: distro mirror or the ondrej/sury PPA
apt-cache policy php8.3-fpm
```

For a distro build, check that its changelog names both CVEs. Read the whole file, without `grep -q`: under `pipefail`, `zcat ... | grep -q` can report a miss on a hit, because grep exits at the first match and zcat dies on the closed pipe.

```bash
zcat /usr/share/doc/php8.3-common/changelog.Debian.gz | grep -E 'CVE-2026-1096(18|19)'
```

Then look for PHP that `dpkg` does not know about. Control panels ship their own builds (cPanel in `/opt/cpanel/ea-php*`, Plesk in `/opt/plesk/php/*`, LiteSpeed as `lsphp`), and containers carry the PHP of their image. A quick check for running PHP outside the package manager:

```bash
for p in $(pgrep -f 'php-fpm|php-cgi|lsphp'); do
  exe=$(readlink -f /proc/$p/exe)
  dpkg -S "$exe" >/dev/null 2>&1 || echo "not from dpkg: $exe (pid $p)"
done
```

Anything this prints needs its own update route, covered under Other platforms below.

## Step 2: compare with the fixed builds

A build is fixed when it is at or above the row for its own source and release. Thresholds never cross sources: a PPA build numbered above Ubuntu's fix is not Ubuntu's fix, and Ubuntu 26.10's unfixed `8.5.9-0ubuntu2` sorts above 26.04's fixed `8.5.4-0ubuntu1.5`.

| Source and release | Branch | Fixed build or higher | Advisory |
| --- | --- | --- | --- |
| php.net, PPA, Remi, Docker official images | 8.5 | `8.5.12` | [PHP security advisory, 21 Oct](https://www.php.net/archive/2026.php#2026-10-21-5) |
| php.net, PPA, Remi, Docker official images | 8.4 | `8.4.27` | same |
| php.net, PPA, Remi, Docker official images | 8.3 | `8.3.36` | same |
| php.net, PPA, Remi, Docker official images | 8.2 | `8.2.35` | same |
| Ubuntu 26.10 | 8.5 | `8.5.9-0ubuntu2.1` | [USN-8948-1](https://ubuntu.com/security/notices/USN-8948-1) |
| Ubuntu 26.04 LTS | 8.5 | `8.5.4-0ubuntu1.5` | USN-8948-1 |
| Ubuntu 24.04 LTS | 8.3 | `8.3.6-0ubuntu0.24.04.13` | USN-8948-1 |
| Ubuntu 22.04 LTS | 8.1 | `8.1.2-1ubuntu2.28` | USN-8948-1 |
| Ubuntu 20.04 (Ubuntu Pro only) | 7.4 | `7.4.3-4ubuntu2.29+esm5` | [USN-8948-2](https://ubuntu.com/security/notices/USN-8948-2) |
| Ubuntu 18.04 (Ubuntu Pro only) | 7.2 | `7.2.24-0ubuntu0.18.04.17+esm14` | USN-8948-2 |
| Ubuntu 16.04 (Ubuntu Pro, Legacy add-on) | 7.0 | `7.0.33-0ubuntu0.16.04.16+esm22` | USN-8948-2 |
| Ubuntu 14.04 (Ubuntu Pro, Legacy add-on) | 5.5 | `5.5.9+dfsg-1ubuntu4.29+esm18` | USN-8948-2 |
| Debian 13 | 8.4 | `8.4.27-1~deb13u1` | [DSA-6574-1](https://lists.debian.org/debian-security-announce/2026/msg00489.html) |
| Debian 12 | 8.2 | `8.2.35-1~deb12u1` | [DLA-4848-1](https://lists.debian.org/debian-lts-announce/2026/10/msg00042.html) |
| Debian 11 | 7.4 | none from Debian | [LTS ended 31 Aug 2026](https://www.debian.org/News/2026/20260831) |

PHP 7.4, 8.0 and 8.1 get no official release from the PHP project, only minimal source backports; 5.4 to 7.3 get nothing. If your row says "none" or needs a subscription you lack, jump to *When no fix exists for your release* below.

## Step 3: update on Debian and Ubuntu

Install the fixed build from the same source the branch already comes from, and pin every package of the branch to that one version. Two shortcuts cause most of the breakage we see:

- `apt-get install --only-upgrade '^php.*'` lets apt pick the highest version anywhere. On a server with Ubuntu's PHP and the ondrej PPA configured, that moves Ubuntu's 8.3.6 to the PPA's 8.3.36 without anyone deciding it.
- Pinning PECL extensions such as `php8.3-redis` to the PHP version makes apt refuse the whole install: they come from their own source package with their own versions.

First refresh the lists and read the candidate:

```bash
sudo apt-get update
apt-cache policy php8.3-fpm   # the fixed build, under the source your installed build lists
```

If `apt-get update` stops with *changed its 'Label' value* for the PPA, apt refuses that source until you accept the change. Check that it is expected, then run `sudo apt-get update --allow-releaseinfo-change-label`.

Then install the fixed build for every package built from the branch's own source:

```bash
BRANCH=8.3
FIXED=8.3.6-0ubuntu0.24.04.13   # your row from Step 2, or the PPA build apt-cache policy shows
PKGS=$(dpkg-query -W -f='${db:Status-Abbrev}|${binary:Package}|${source:Package}\n' \
  "php$BRANCH*" "libapache2-mod-php$BRANCH" 2>/dev/null \
  | awk -F'|' -v s="php$BRANCH" '$1 ~ /^ii/ && $3 == s {print $2}')
sudo cp -a /etc/php "/root/etc-php-$(date +%F)"   # config backup before the change
sudo apt-get install $(for p in $PKGS; do printf '%s=%s ' "$p" "$FIXED"; done)
```

Repeat per branch. A branch that no site uses is better stopped and removed than patched: one less thing to keep current.

If you want a real way back, keep the installed packages before the upgrade: `sudo apt-get install dpkg-repack`, then `dpkg-repack $PKGS` in a folder under `/root`. Ubuntu and Debian keep only the newest revision in their security pockets, so `apt-get install pkg=old-version` usually finds nothing once the fix is out.

## Other platforms in brief

The principle is the same everywhere: update through the channel that installed PHP, every version on the box, then restart.

- **RHEL, AlmaLinux, Rocky with Remi.** On a supported stream (`php:remi-8.3`, `php:remi-8.4`), `sudo dnf upgrade --refresh 'php*'` is enough. On an end-of-life stream, `dnf upgrade` will not leave it: switch with `dnf module reset php` and `dnf module enable php:remi-8.4`, then `dnf distro-sync`, after testing the application on 8.4. Software Collections (`php74-php-fpm`, `php81-php-fpm`) are separate packages and need their own update.
- **cPanel, Plesk and similar panels.** They ship their own PHP builds per version and per site, so `php -v` in the shell says nothing about the PHP a given site runs. Use the panel's own updater, then check every version a site is assigned to.
- **Docker and Kubernetes.** Never patch inside a running container: the next restart brings the old image back. Rebuild from a fixed tag such as `php:8.4.27-fpm` and pin it by digest (`php:8.4.27-fpm@sha256:...`); a `latest` tag tells you nothing about what you deployed. In official images `php -v` is honest, because they build upstream releases.

## Step 4: restart every worker, then prove it

A fixed package on disk does not fix the server: each PHP-FPM pool and each Apache with mod_php keeps running the code it started with. A service that started before the last PHP install still serves the vulnerable build.

Compare the start time of every PHP service with the newest PHP install:

```bash
for u in $(systemctl list-units --type=service --state=running --plain --no-legend 'php*-fpm*' apache2 | awk '{print $1}'); do
  echo "$u started $(systemctl show -p ActiveEnterTimestamp --value "$u")"
done
ls -lt --time-style=full-iso /var/lib/dpkg/info/php*.list | head -3   # newest PHP install
```

Restart each branch's pool, and Apache when it loads mod_php:

```bash
sudo php-fpm8.3 -t && sudo systemctl restart php8.3-fpm
sudo apachectl configtest && sudo systemctl restart apache2   # mod_php only
```

Run the config test first: a failing config takes the pool down on restart. An Apache restart stops every site on the server for a few seconds, so schedule it. nginx needs no restart. Afterwards, `sudo needrestart -r l` lists any service still holding old PHP libraries.

Finish with the HTTP status of your main pages, before and after: a site that answered 200 and now answers 502 points at a pool that did not come back.

## Step 5: turn off session upload progress

Set `session.upload_progress.enabled = 0` on every server, patched or not. On 23 October PHP warned of CVE-2026-115560, a further RCE path through session upload progress that the October releases do not fix. PHP calls it unlikely: it needs an application that switches `session.serialize_handler` at runtime. Still, PHP recommends the setting until its next regular release, and suggests keeping it off afterwards if nothing needs it. On unpatched builds it also closes the session path of CVE-2026-109619. Ordinary file uploads keep working; only upload progress bars that rely on this feature stop.

Put it in each web SAPI's own config, not the CLI's:

```ini
; /etc/php/8.3/fpm/conf.d/99-security.ini  (and /etc/php/8.3/apache2/conf.d/ for mod_php)
session.upload_progress.enabled = 0
```

Do not set it with `ini_set()` or in `.user.ini`: both apply too late in the request to count. In the other direction, a site can turn it back on through `.htaccess` (mod_php) or a `fastcgi_param PHP_VALUE` in nginx. Where you do not control the sites, lock it in the pool or vhost instead:

```ini
; /etc/php/8.3/fpm/pool.d/www.conf
php_admin_flag[session.upload_progress.enabled] = off
```

Then test, restart and check the value the SAPI actually uses:

```bash
sudo php-fpm8.3 -t && sudo systemctl restart php8.3-fpm
php-fpm8.3 -i | grep 'session.upload_progress.enabled'   # expect Off
```

A PHP version you install next month starts with upload progress on again. Add the file to your provisioning, or re-check after every PHP install.

## When no fix exists for your release

If your release gets no free fix, `apt-get update` will never bring one; the way out is a different source, a subscription or a newer release.

| Situation | Route | Cost |
| --- | --- | --- |
| Debian 11, Debian's php7.4 | Move to Debian 12 or 13; meanwhile the sury php7.4 build `1:7.4.33-31` from packages.sury.org, which backports both fixes, or Freexian ELTS | Release upgrade; or PHP from a third-party repo; or a paid subscription |
| Ubuntu 20.04 or 18.04 | Ubuntu Pro: `sudo pro attach`, then `sudo pro fix CVE-2026-109618` and `CVE-2026-109619` | Free for personal use on up to 5 machines, paid for business |
| Ubuntu 16.04 or 14.04 | Ubuntu Pro with the Legacy add-on, or a release upgrade | Paid |
| PHP 7.4 to 8.1 from any source | Migrate to 8.3 or 8.4; check first with `composer check-platform-reqs` | Code and plugin compatibility work |

Until the route is in place, narrow the attack surface. These controls come from PHP's own advisories ([GHSA-w8hw-w9fx-j79w](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w) and [GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)). PHP is clear they are not a fix, and each has a price:

```ini
; /etc/php/7.4/fpm/pool.d/www.conf - admin values, so ini_set() cannot change them
php_admin_flag[session.upload_progress.enabled] = off
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M        ; 32M if the site takes uploads
php_admin_value[upload_max_filesize] = 7M  ; keep it below post_max_size
pm.max_requests = 1
```

- `memory_limit = -1` narrows the out-of-memory path of CVE-2026-109618 but does not close it: an allocation the system refuses still takes the vulnerable route. It also lets one runaway request eat the server's RAM. Do not use `RLIMIT_AS` or `RLIMIT_DATA` against that: they trigger the same vulnerable failure. A plain `memory_limit` in php.ini does not hold either, because applications such as WordPress raise it with `ini_set()`.
- A cgroup cap (`MemoryMax=` via `sudo systemctl edit php7.4-fpm`) is not a fix either, and PHP warns that killing the whole group takes down the FPM master with every worker. systemd stops the whole unit after an OOM kill by default ([`OOMPolicy=`](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#OOMPolicy=), default `stop`), so add `OOMPolicy=continue` next to `MemoryMax=` if you set one: then the kernel kills one worker and the pool keeps running.
- `pm.max_requests = 1` recycles each worker after one request, which shortens how long it carries stale state from CVE-2026-109619. PHP says recycling lowers the odds but does not remove them. Expect noticeably more CPU and slower responses.
- On an unpatched PHP 8.5 add `fatal_error_backtraces = 0`: it removes the confirmed RCE path of CVE-2026-109619 on that branch.
- If the application takes no multipart forms, block multipart requests at the proxy, and keep `max_input_nesting_level` low: multipart parsing is how the out-of-memory condition gets triggered.
- A WAF in front buys time: AWS WAF's managed PHP rule set (`AWSManagedRulesPHPRuleSet`) covers PHP2Shell, and other providers ship their own rules. AWS itself calls it defense in depth, not a replacement for the upgrade.

## Step 6: check that you were not hit already

Patching closes the door, but it does not remove anyone already inside. Amazon saw the first exploitation attempt on 17 October, four days before the fixes, and advises investigating from that date rather than from the disclosure. Look at everything since then.

1. **Crashes.** The exploit forces out-of-memory errors at the wrong moment, and attempts that miss tend to crash the worker. A burst of segfaults is the clearest trace:

   ```bash
   sudo journalctl -k --since 2026-10-17 | grep -i segfault | grep -Ei 'php|apache2|httpd'
   sudo zgrep -h 'exited on signal 11' /var/log/php*-fpm.log*
   sudo zgrep -h 'Segmentation fault' /var/log/apache2/error.log*
   ```
2. **Shells under PHP.** A web server has no reason to start a shell or a downloader:

   ```bash
   ps -eo ppid=,pid=,comm=,args= | while read -r pp p c a; do
     pc=$(ps -o comm= -p "$pp")
     case $pc in php-fpm*|apache2|httpd)
       case $c in sh|bash|dash|nc|ncat|curl|wget|python*|perl) echo "$pc ($pp) -> $a" ;; esac ;;
     esac
   done
   ```
3. **New PHP files.** Web shells land in the document root or in temp folders:

   ```bash
   sudo find /var/www /tmp /var/tmp /dev/shm -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \) \
     -newermt 2026-10-17 -printf '%TY-%Tm-%Td %p\n' | sort -r | head -50
   ```

   A CMS or plugin update creates new files too, so read the list rather than count it. A clean result is not proof: file times can be forged.

If any check finds something, treat the server as compromised. Isolate it, rebuild it from a clean image rather than cleaning it, and rotate every secret PHP could read: database passwords, API keys, session secrets and TLS keys. PHP lists memory disclosure as a confirmed impact of CVE-2026-109619, so those secrets may already be out even without a web shell.

## Keep it patched

Ubuntu's and Debian's own PHP updates itself through unattended-upgrades, where it is enabled (the default on Ubuntu, not on every Debian install); PHP from the ondrej PPA does not. unattended-upgrades accepts only the distro's security pockets by default, so a PPA server stays on whatever build was installed until someone upgrades it by hand.

Adding the PPA to unattended-upgrades fixes that, at a price: the PPA has no security-only section, so the server then installs every new PPA build of PHP and its libraries on its own schedule and restarts PHP-FPM as it does. Before you add it, pin any distro-built PHP branch on the same server away from the PPA (`Pin-Priority: -1` in a file under `/etc/apt/preferences.d/` ending in `.pref`; apt silently skips other extensions), or the distro branch will start pulling PPA builds.

A short checklist per server:

- [ ] Every PHP package at or above its row in Step 2, from its own source
- [ ] Every PHP-FPM pool and mod_php Apache restarted after the last PHP install
- [ ] `session.upload_progress.enabled` reports Off in every web SAPI
- [ ] No PHP outside the package manager left unpatched (panels, containers, `/usr/local`)
- [ ] Crash, process and file checks since 17 October done and clean
- [ ] A plan for every branch or release with no free fix
- [ ] Automatic updates confirmed for the source each PHP comes from

## How Mettevo can help

We audit and patch PHP servers for clients in three stages: a read-only audit first, then the patch in an agreed window, then a report per server.

- **Audit.** Every PHP branch and SAPI on the server, its exact build and source, whether the running workers use it, the upload progress setting, and the checks from Step 6. Nothing on the server changes.
- **Patch.** The pinned upgrade from the branch's own source, the mitigation, config tests, restarts of only what changed, and an HTTP check of your sites before and after, with rollback steps written down.
- **Plan.** For servers with no free fix: the cheapest safe route, whether that is Ubuntu Pro, a release upgrade or a move to PHP 8.3 or 8.4.

The audit and patch tooling we use is open source: [php2shell-fixer](https://github.com/turovskiy/php2shell-fixer) on GitHub. If you would rather hand it over, [get in touch](https://mettevo.com/contacts) and tell us how many servers you run.

## Sources

- [PHP: security advisory, 21 October 2026](https://www.php.net/archive/2026.php#2026-10-21-5) - fixed releases, affected SAPIs, backports for 7.4 to 8.1, mitigations
- [PHP: informational advisory on session upload progress, 23 October 2026](https://www.php.net/archive/2026.php#2026-10-23-1) - CVE-2026-115560
- [GHSA-w8hw-w9fx-j79w](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w) and [GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp) - root cause, CVSS, mitigations and their limits
- [CISA Known Exploited Vulnerabilities catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog?search_api_fulltext=CVE-2026-109618) - added 26 October, due 29 October
- [Amazon Threat Intelligence: PHP2Shell exploitation prior to public disclosure](https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/) - first attempt on 17 October
- [USN-8948-1](https://ubuntu.com/security/notices/USN-8948-1) and [USN-8948-2](https://ubuntu.com/security/notices/USN-8948-2) - fixed Ubuntu builds, Ubuntu Pro for 20.04 and older
- [DSA-6574-1](https://lists.debian.org/debian-security-announce/2026/msg00489.html) and [DLA-4848-1](https://lists.debian.org/debian-lts-announce/2026/10/msg00042.html) - fixed Debian 13 and 12 builds
- [Debian 11 Long Term Support reaches end-of-life](https://www.debian.org/News/2026/20260831) - no Debian fix for 11
- [AWS: WAF mitigations for PHP2Shell](https://aws.amazon.com/blogs/security/using-aws-waf-to-apply-mitigations-for-php2shell-rce-vulnerabilities-cve-2026-109618-and-cve-2026-109619/) - managed rule set and its limits
- [systemd.service: OOMPolicy=](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#OOMPolicy=) - what systemd does to a unit after an OOM kill
- [NCSC-NL: php2shell operational information](https://github.com/NCSC-NL/php2shell) - mitigations are not fixes, backported packages
