跪拜 Guibai
← Back to the summary

php2shell: PHP Patches Two Unauthenticated RCEs with a Single Corrupted-Memory Root Cause

PHP Vulnerability Crisis: Has Your Website Upgraded?

On October 21, 2026, the PHP team released 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, fixing two unauthenticated RCE vulnerabilities rated CVSS v4.0 9.2 (named php2shell by the research team). Unauthenticated, one HTTP request, RCE completed before application code executes. It affects all versions since 5.4.

CVE Root cause Officially confirmed exploitation path
2026-109618 When OOM occurs, longjmp interrupts, half-initialized memory is consumed by cleanup mod_php default config, unauthenticated request triggers directly
2026-109619 Worker global state not reset, steps on memory freed by a previous request session.upload_progress; fatal error backtrace
Branch Security line Status
8.5 8.5.12 Fixed
8.4 8.4.27 Fixed
8.3 8.3.36 Fixed
8.2 8.2.35 Fixed
8.1 and earlier No patch EOL, migrate or isolate

Exposure data (all from reports around October 21, 2026; order-of-magnitude reference):

Vulnerability details: two holes, one root cause

CVE-2026-109618 (OOM corruption): PHP uses longjmp() to abort when a request exceeds memory_limit. On the abort path, internal request data may be only half-initialized, and the request shutdown cleanup treats this data as valid and frees memory that was never initialized. An attacker can trigger it by constructing a multipart upload request. The official team verified a full unauthenticated exploit under Apache mod_php's default configuration. In the demo video released by Calif, from sending the request to getting a shell takes about 15 seconds.

CVE-2026-109619 (residual global state): a worker's global state holds pointers to memory freed by a previous request, and the next request handled by the same worker accesses this stale state before normal initialization resets it, forming a use-after-free. Two paths are officially confirmed: session.upload_progress (both mod_php and FPM) and fatal error backtraces (PHP 8.5).

Two holes, one root cause: memory is left in a corrupted state at the boundary where a request terminates abnormally.

# Temporary mitigation (not a full fix): limit decoded request body size at the front end
client_max_body_size 8m;   # for services that do not accept file uploads
# Note: do not use RLIMIT_AS / RLIMIT_DATA to replace memory_limit;
# the official warning says this induces mmap failure and lands exactly on the longjmp fragile path

Why it stayed buried for more than a decade

In 2024, the PHP Foundation funded Quarkslab for an external audit. Core developers noticed the initial signs of OOM at the time. In theory an exploit could be written, but no concrete path was found, so the issue was shelved. The foundation's retrospective, in its own words: "This delay was a structural failure."

The turning point came this year: on July 30, Calif started an audit; on August 11 it reported an unconditional RCE; OpenAI independently reported the same root cause on the same day; and Wiz's report arrived on August 24. All three used frontier AI models, and Wiz used Wiz Atlas, Gemini 4 Argon, and GPT-5.6 Sol throughout. In Wiz's words: "Frontier AI turned a long-standing flaw into a usable exploit."

Upgrade in practice: seven steps, don't skip a single one

Someone might toss out: "Just upgrade first, stop asking questions." Upgrading is necessary, but a half-baked upgrade that only swaps binaries without restarting is the same as not upgrading. Follow the seven steps.

Step 1: Verify versions. Don't trust the control panel or the package manager.

Only what the command line prints counts. CLI and FPM may not be the same version; check both:

php -v
php-fpm -v

# How many PHP installs are on the machine, and which one is current
which php
ls -l /usr/bin/php*
update-alternatives --display php

# What packages and versions the package manager sees
dpkg -l | grep -E '^ii +php' | awk '{print $2, $3}'
rpm -qa | grep php

# For Docker, check inside the container; don't trust the image tag
docker exec -it <container> php -v

If you use panels like BaoTa, phpStudy, or oneinstack, note that the version shown in the panel is not the version FPM is actually running. Trust php-fpm -v. A real case: the panel showed 8.2 while FPM was actually running 7.4. Upgrading according to the panel accomplished nothing, and the service was restarted more than ten times without anyone realizing it.

Step 2: Back up. Two minutes.

# Back up the whole config directory; -a preserves permissions and ownership
cp -a /etc/php /etc/php.bak.$(date +%F)

# Keep a full config snapshot before the upgrade
php -i > /root/php-info-before.txt
dpkg -l | grep php > /root/php-pkgs-before.txt

Three years ago, a minor-version upgrade was done without a backup; the new package overwrote the config, one opcache parameter was missing when the config was rewritten from memory, performance dropped by half, and it took more than two hours to find. These two cp commands should be run without thinking before any upgrade.

Step 3: Upgrade. Pick the path for your scenario.

Scenario A: Debian / Ubuntu, the distro repo already has the new version:

apt update
apt install --only-upgrade \
  php8.3-fpm php8.3-cli php8.3-common

If the repo does not have it yet, add the ondrej PPA:

apt install software-properties-common
add-apt-repository ppa:ondrej/php
apt update

Scenario B: RHEL / Rocky / Alma, use the Remi repo:

dnf install \
  https://rpms.remirepo.net/enterprise/remi-release-9.rpm
dnf module reset php
dnf module install php:remi-8.3

Do not skip the module reset step, or dnf will fight you with the system default stream.

Scenario C: Docker. Change the Dockerfile; don't manually upgrade inside the container. Pin the minor version in the tag and rebuild with --no-cache. Reusing a layer with the old version is the biggest way to waste an upgrade:

docker compose build --no-cache
docker compose up -d
docker exec -it <container> php -v

Scenario D: Compiled install. Keep the old install directory, install the new version into a new directory, switch traffic with a symlink, and switch back if anything goes wrong.

A pitfall common to all four scenarios: distribution repositories sync with a delay. The official announcement can be out while the repo does not yet have the package. If you can't wait, switch to a nearby mirror instead of sitting idle.

Step 4: Restart. This is the disaster zone.

Swapping binaries without restarting workers is the same as not upgrading. Note: restart, not reload. Reload only re-reads configuration; it does not replace old code in processes.

# Nginx + PHP-FPM, service names vary by distro; try each
systemctl restart php8.3-fpm
systemctl restart php-fpm
systemctl restart php-fpm.service

# Apache mod_php
systemctl restart apache2    # Debian family
systemctl restart httpd      # RHEL family

# K8s: after a rolling restart, confirm the old ReplicaSet is scaled to zero
kubectl rollout restart deployment/php-app
kubectl rollout status deployment/php-app
kubectl get rs

# Final safety check: see whether the process start time really changed
systemctl show php8.3-fpm -p MainPID
ps -o lstart= -p <PID from above>

If the time is still pre-upgrade, the restart did not take; go back and check the service name.

Step 5: Verify. Upgrading isn't done when the upgrade finishes.

Version verification, check both CLI and FPM; missing either one means the upgrade is not complete:

php -v
php-fpm -v

The case where CLI is upgraded but FPM is not is real: a few years ago, after an upgrade, CLI was on the new version, health checks were green, logs were clean, and the job was closed out. Three days later, a casual php-fpm -v showed the old version. The business had been running on the old process the whole time; only the command line had been upgraded.

Config verification, confirm the existing config was not wiped by the upgrade:

php -i | grep -E \
  'memory_limit|post_max_size|upload_max_filesize'

Business verification, health checks plus logs, and manually click through core functions like upload, login, and payment:

curl -sS -o /dev/null -w '%{http_code}\n' https://your-site/
tail -f /var/log/php8.3-fpm.log

Step 6: If you cannot upgrade, apply mitigations per vulnerability.

For EOL branches and the oldest machines nobody dares to touch, copy the official mitigation configuration exactly.

For the CVE-2026-109618 OOM hole, set memory_limit to -1 (so the OOM path is not triggered) and shrink post_max_size:

; /etc/php/8.3/fpm/php.ini or pool.d/www.conf
php_admin_value memory_limit -1
php_admin_value post_max_size 8M

For services that do not accept file uploads, cap it at 8M; for those that do, 32M. 32M is the upper limit; don't exceed it.

For the CVE-2026-109619 residual-state hole, two switches:

; Turn off upload progress sessions
session.upload_progress.enabled = 0
; PHP 8.5 adds this one
fatal_error_backtraces = 0

Put them in php.ini or the FPM pool config. Don't use ini_set(): it takes effect only after request parsing and will not block this vulnerability. After editing, restart FPM with systemctl restart, then use php -i to confirm the config was actually read.

Two red lines, copied from the official statement: never use RLIMIT_AS or RLIMIT_DATA to replace memory_limit. Both induce mmap failure, which lands exactly on the fragile longjmp path.

Step 7: Rollback plan. Think it through before you start.

# Config rollback
cp -a /etc/php.bak.$(date +%F)/* /etc/php/
systemctl restart php8.3-fpm

# Package-level rollback (Debian family; fill in the old version from your backup)
apt install --allow-downgrades php8.3-common=8.3.35-1

The safer approach is blue-green: start a new pool or new container on the new version, pass health checks, then switch traffic. Keep the old environment in place and tear it down only after the observation period.

Compromise triage and pinning versions in CI

Triage checklist (indicators from Talos and Rescana):

If any of these match, isolate first, then investigate; preserve logs as evidence and don't clear them in a hurry.

Hard CI assertions: fail immediately below the security line.

# Pin the minor version in the Dockerfile
FROM php:8.3.36-fpm-alpine

# Add this to CI
php -v | grep -qE 'PHP 8\.3\.(3[6-9]|[4-9][0-9])' || exit 1

The AI attack-defense debate

AI helped find the vulnerabilities, but whether it helps attack or defense more has no conclusion. On the defensive side, Wiz, Calif, OpenAI, and Gemini turned a decade-old bug into a public record with a patch. On the offensive side, Cloudflare saw attackers modifying payloads to bypass WAF, and watchTowr saw active exploitation attempts. The same knife has been sharpened faster.

Sources

Version-checking scripts, PHP mitigation config templates, and the triage checklist are packaged and available at the end of the post.

Files shared via cloud drive: 2026-10-22 PHP-RCE vulnerability link: https://pan.baidu.com/s/1dWxv7YpixzEUzX2Fir8Rig?pwd=3wss Extraction code: 3wss

Original tech blog · Open-source project sharing · AI full-stack creation community idao.fun

Comments

Top 3 of 4 from juejin.cn, machine-translated. The original thread is authoritative.

数字农民工

You only realize how powerful PHP is when a vulnerability like this comes out 👍

iDao技术魔方

Indeed, there are way too many old websites and assorted open-source systems.

AI向善

We cut PHP's default timeout from 30 seconds to 8 seconds, and the failure rate actually went down. What do you set for production?

爱上咖啡香

They didn't say whether this vulnerability can be exploited to get a shell.