A single Bash script that **finds every PHP on a Linux server, tells you whether it is
vulnerable to CVE-2026-109618 / CVE-2026-109619, and — on Debian and Ubuntu — fixes it
safely and proves the sites still work afterwards.**
It works for any PHP server: plain Apache or nginx sites, WordPress, Nextcloud, Laravel,
control panels (cPanel, Plesk, CloudLinux, DirectAdmin, aaPanel), LiteSpeed, FrankenPHP
and PHP running inside Docker/Podman containers.
> **Default is read-only.** Without `--fix` it only writes its own log and refreshes apt's
> package lists. With `--fix` it shows its plan and asks before changing anything.
| CVE-2026-109618 | Out-of-memory handling can interrupt PHP while request data is only partly initialised ([GHSA-w8hw-w9fx-j79w]( |
| CVE-2026-109619 | Request-owned state can survive after its memory was freed (use-after-free) ([GHSA-6wmf-4rv5-p3rp]( |
| Impact | Unauthenticated remote code execution as the web server user. CVSS v4 9.2 (critical). |
| Confirmed | Complete RCE over HTTP against **Apache mod_php**; validated against **PHP-FPM** for the session path; LSAPI and FrankenPHP named as likely candidates. |
| Alert | [NCSC (UK)]( · product inventory: [NCSC-NL php2shell]( |
| 8.1 and older | **no fix** (end of life) – upgrade the branch |
Distributions often *backport* the fix and keep the old version number (for example
Ubuntu's `8.3.6-0ubuntu0.24.04.13`). The script detects this from the package changelog,
so a backported build is reported as patched, and an old build is not hidden behind a
The advisories say updating is the **only complete fix**. As extra hardening they
recommend `session.upload_progress.enabled = 0`, which this script sets on Debian/Ubuntu
(skip it with `--no-harden` if an application uses upload progress).
# 1. download a release and check it (compare with SHA256SUMS on the release page)
curl -fsSLO
# 2. read it – it is one file, and it is going to run as root
# 4. fix (Debian/Ubuntu): shows a plan and asks before changing anything
Please do not pipe it straight into a root shell (`curl … | sudo bash`). Download it,
look at it, then run it. Try `--fix` on a staging copy first if you have one.
**1. Finds every PHP engine** — packaged ones (`dpkg`, `rpm`, `apk`) and everything
else in the usual places: `/usr/local`, `/opt/remi`, `/opt/cpanel/ea-php*`,
`/opt/plesk/php`, `/opt/alt/php*`, `/usr/local/lsws/lsphp*`, `/www/server/php`,
FrankenPHP and Apache `libphp*.so` modules. It also inspects running processes, so it
also catches PHP inside containers, and Apache workers that still have an old `mod_php`
**2. Decides for each engine**: patched, patched by a distribution backport, vulnerable,
end-of-life, or cannot verify. CLI-only binaries that are not running are reported as
warnings. Web-facing engines (FPM, mod_php, CGI, LSAPI, FrankenPHP) are reported as
- upgrades the PHP packages of every branch in use, including extensions and the
- repairs apt pins that silently block PHP updates from known vendors (`packages.sury.org`,
Ondřej's PPAs) — for example a `Package: *` pin at priority 10, or a `php8.3-*` pin
- restarts only the services whose PHP changed, or that still run a replaced binary,
- optionally (`--purge-unused`) removes PHP versions nothing uses.
Ubuntu PPAs configured on Debian are offered for disabling. With
`--disable-failing-repos`, broken repos that no installed package depends on are
- services are running and configuration tests pass;
- every site name in the web server config still answers on this machine, compared with
- Nextcloud: `occ` status, database, `status.php`, background jobs, setup checks and
**5. Looks for compromise indicators** (read-only):
- typical web-shell code in recently changed PHP files;
- PHP files in the Nextcloud data directory (users' own files are excluded);
- shells or network tools spawned by PHP workers;
These are hints, not proof. A clean result does not prove the server was not
On other systems (RHEL/Alma/Rocky, CloudLinux, cPanel, Plesk, Alpine, containers,
source builds) the script audits and prints the **exact update command** for each
vulnerable engine instead of changing anything.
| Debian 10–13, Ubuntu 20.04–24.04 (distribution PHP, Sury, Ondřej PPA) | ✅ | ✅ |
| RHEL / Alma / Rocky / CentOS / Oracle / CloudLinux / Amazon Linux (incl. Remi) | ✅ | prints commands |
| cPanel EA-PHP, Plesk PHP, CloudLinux alt-php, LiteSpeed lsphp, aaPanel | ✅ | prints commands |
| PHP built from source, FrankenPHP | ✅ | prints instructions |
| PHP inside Docker/Podman containers (seen from the host) | ✅ | prints instructions (rebuild image) |
This tool runs as root on production servers, so it is built to be boring:
- **Read-only by default.** `--fix` shows a plan and asks; non-interactive runs need
`--yes` as well. An audit also works without root (with less visibility).
- **Every package change is simulated first.** If apt would remove any non-PHP package,
the change is refused. Package configuration files you edited are kept
- **Backups before changes:** `/etc/apt`, `/etc/php`, `/etc/apache2`, `/etc/httpd` and
`/etc/nginx`, plus the list of PHP package versions, go to
`/var/backups/php-cve-2026-fix//` (root-only — they can contain secrets such
- **Configuration tests before every restart.** If PHP-FPM rejects the hardening file,
- **Never runs website code or untrusted binaries as root.**
- PHP versions are read from the binary file, not by executing it.
- Nextcloud's `occ` runs as the owner of `config.php` with a clean environment, and is
- System tools such as `php-fpm -t` are only executed when the binary and every parent
directory are root-owned and not writable by others.
- Nothing from inside a container is executed; its files are only read, with size
- **No downloads, no telemetry.** Network use is apt itself, HTTP checks against
`127.0.0.1` (proxies are bypassed so host names never leave the machine), and any
- refuses a `PATH` with relative or world-writable entries;
- uses a private temp directory, and writes logs and reports without following
- time limits on directory walks, size limits on files it reads.
What it **changes** with `--fix` (and nothing else):
| PHP packages via `apt-get install pkg=version` | the actual fix |
| `/etc/apt/preferences.d/00-php-cve-2026-fix-phpX.Y.pref` | only when a pin blocked a known PHP vendor |
| `/etc/php/X.Y/{fpm,apache2,cgi}/conf.d/99-zz-php-cve-2026-hardening.ini` | `session.upload_progress.enabled = 0` (skip with `--no-harden`) |
| apt source files | only for Ubuntu PPAs on Debian, or with `--disable-failing-repos`; a copy is kept |
| `phpX.Y-fpm` / `apache2` restarts | only for changed or stale PHP |
| purge of unused PHP | only with `--purge-unused` |
| `/var/log/php-cve-2026-fix/`, `/var/backups/php-cve-2026-fix/` | log and backup |
tar xzf "$B/etc-snapshot.tgz" -C / # configs as they were
cat "$B/php-packages-before.txt" # versions before; apt-get install pkg=version to go back
rm /etc/php/*/*/conf.d/99-zz-php-cve-2026-hardening.ini # just the hardening
--fix Apply fixes (Debian/Ubuntu). Shows a plan and asks first.
-y, --yes With --fix: do not ask (for automation).
--purge-unused Remove PHP versions nothing uses (no web config, no process,
not the CLI default, not referenced in cron/systemd).
--disable-failing-repos Also disable apt repos that fail to update, unless an
--no-harden Do not set session.upload_progress.enabled=0.
--nextcloud PATH Nextcloud directory (repeatable; auto-detected otherwise).
--url URL Extra URL that must not return 5xx after changes (repeatable).
--days N Look-back window for compromise indicators (default 7).
--no-ioc Skip the compromise-indicator scan.
--no-update Do not run 'apt-get update'.
--json FILE Write a machine-readable report.
--log-dir DIR Log directory (default /var/log/php-cve-2026-fix as root).
**Exit codes:** `0` all good · `1` warnings · `2` failures (for example still vulnerable)
sudo ./php-cve-2026-fix.sh # audit
sudo ./php-cve-2026-fix.sh --fix # plan, confirm, fix
sudo ./php-cve-2026-fix.sh --fix --yes --purge-unused # unattended
sudo ./php-cve-2026-fix.sh --fix --url
sudo ./php-cve-2026-fix.sh --json /root/php-cve.json # for dashboards
ssh "$h" "sudo /tmp/php-cve-2026-fix.sh --no-color --json /tmp/php-cve.json; echo exit=\$?"
- **Nextcloud:** found automatically under the web roots, `/var/www`, `/srv`, `/opt` and
`/usr/ `; pass `--nextcloud /path` otherwise. Users' own files in the data
directory are never scanned, so a website project someone stores in Nextcloud is not
reported as a web shell. The Talk "high-performance backend" setup notice is shown as
- **TurnKey Linux / appliances with Sury pins:** a `php-sury.pref` that pins
`packages.sury.org` to priority 10 keeps PHP on the old build forever. The script
detects this and adds a narrow pin for the PHP branch in use.
- **Apache mod_php:** this is the SAPI the advisories exploited. The script warns
- **Docker/Podman:** the host sees PHP processes inside containers and reports their
version, read from the binary. The fix is a new image: for example pull an updated
`php:8.3-fpm` tag, rebuild, and recreate the container.
- **End-of-life OS:** if the distribution is out of support (for example Debian 11 after
August 2026), the audit warns. Third-party PHP repos can stop building for it at any
shellcheck -S warning php-cve-2026-fix.sh tests/*.sh
# scenario tests (stubbed apt/dpkg) – they write to /etc and /usr, so only in a container:
docker run --rm -v "$PWD":/src -w /src debian:12 bash -c \
'apt-get update -qq && apt-get install -y -qq python3 curl procps >/dev/null && tests/run-tests.sh'
# integration test on a real system (installs apache2 + php-fpm) – CI or a throw-away VM:
CI runs shellcheck, the scenario tests on Debian 11/12 and Ubuntu 22.04/24.04, and the
integration test on a real Ubuntu runner (see `.github/workflows/ci.yml`).
If PHP publishes further fixed releases, update the `FIXED` table and `CVE_IDS` near the
Found a problem in the script itself? Please report it privately (see
[SECURITY.md](SECURITY.md)), not in a public issue.
Provided "as is", without warranty of any kind (see [LICENSE](LICENSE)). You are
responsible for what you run on your servers. The compromise checks look for common
traces only and cannot prove that a server is clean. If you suspect a compromise,
follow your incident-response process and your national CERT's guidance.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
