Your host emails you: PHP shipped a security release, and your WordPress site on Apache with mod_php runs a version older than the fixed one. Your application code is clean and your plugins are current, yet the advisory says that does not matter. This article explains what PHP2Shell is, what is and is not confirmed about it, and how to verify your version, test interim hardening and detect a suspicious worker in a local Docker lab. The lab contains no exploit code. Everything runs on 127.0.0.1 for authorized, educational use.
What is PHP2Shell?
PHP2Shell is the nickname for two critical flaws in PHP itself, published on 2026-10-21 by the PHP project (NVD and the php-src GitHub advisories). Both concern how PHP handles request memory, so they sit below your application.
| CVE-2026-109618 | CVE-2026-109619 | |
|---|---|---|
| PHP’s title | RCE via OOM corruption | RCE via stale global state |
| Root cause | longjmp() out of an out-of-memory error leaves request memory uninitialised; request cleanup then reads it |
Global state keeps pointing at freed request memory; a later request on the same worker uses it |
| Trigger noted by PHP | Several multipart parsing behaviours can cause OOM within apparent limits | A "producer" request and a "consumer" request on the same worker |
| CVSS v4.0 | 9.2 Critical | 9.2 Critical |
Both carry the vector CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N. Note AC:H and AT:P: the score is critical, but the vector says exploitation is not trivial.

Impact
PHP’s advisories confirm unauthenticated remote code execution as the web worker account. The CVE-2026-109618 exploit was validated over HTTP against mod_php with default input and memory limits. The advisory for CVE-2026-109619 also lists heap use-after-free and memory disclosure as confirmed impact. On a typical WordPress or shared-PHP host, code running as the worker user can read the site’s files, including wp-config.php with its database credentials, and use the database. Clean application code does not help: the corruption happens in PHP’s request startup and shutdown. Varonis and Recorded Future go further and report that attackers can read secrets held in worker memory, such as session data, tokens and API keys. I did not verify that claim here, so treat it as reported, not confirmed. If a worker may have been hit, rotate those secrets anyway.
What I could and could not confirm
I checked the owner’s notes against NVD, the two php-src advisories and the vendor posts. Where sources disagree, I say so.
| Claim | Status |
|---|---|
| CVE IDs and the "memory corruption" / "stale state" split | Confirmed (NVD, GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp) |
| CVSS v4 9.2 Critical | Confirmed, with the vector above |
| Disclosure around 2026-10-21 | Confirmed: NVD and GHSA publication date. Recorded Future says PHP pre-announced the release on 2026-10-20 |
| Fixed in 8.5.12, 8.4.27, 8.3.36, 8.2.35 | Confirmed |
| Affects PHP back to 5.4 | Not confirmed. Varonis says so; NVD and PHP’s two advisories list only branches 8.2 to 8.5. I found no mention of older branches or backports in the advisories |
| Unauthenticated memory read of a worker | Partly confirmed. GHSA-6wmf-4rv5-p3rp lists "memory disclosure" among the confirmed impacts of CVE-2026-109619. The specifics (passwords, tokens, API keys in a worker) come from Varonis and Recorded Future (citing Wiz), and I could not verify them myself |
| RCE "needs an ASLR bypass" | Not found in any source I read. Varonis says one bug helps an attacker "defeat address uncertainty" |
| Public PoCs incomplete or version-specific | Partly supported: Recorded Future says their effectiveness varies and some rest on a theory later invalidated. The wiki I checked does not match PHP’s root cause for CVE-2026-109618 |
| Detection: worker spawns a child while parsing an upload | Reported by Varonis as a high-confidence signal. It is not in PHP’s advisories |
PHP names Apache mod_php and FrankenPHP as most exposed and PHP-FPM as susceptible based on source analysis. For CVE-2026-109619, the strongest demonstrated case is the PHP 8.5 fatal_error_backtraces feature against mod_php.
Lab setup: unpatched and fixed PHP side by side
The lab uses official php:<version>-apache images: 8.4.26 (affected) and 8.4.27 (fixed). Apache with mod_php is the configuration PHP calls most exposed. The tag is a build argument, so one Dockerfile builds both.
ARG PHP_TAG=8.4.26-apache
FROM php:${PHP_TAG}
COPY www/ /var/www/html/ # only harmless demo pages are served
COPY tools/ /opt/lab/tools/ # monitor and scripts: never served over HTTP
The compose file binds the port to loopback and adds a sidecar that shares the web container’s PID namespace, so it can watch worker processes.
services:
web:
build: { context: ., args: { PHP_TAG: "${PHP_TAG:-8.4.26-apache}" } }
image: php2shell-lab:${PHP_TAG:-8.4.26-apache}
ports: ["127.0.0.1:8080:80"]
monitor:
image: php2shell-lab:${PHP_TAG:-8.4.26-apache}
pid: "service:web"
depends_on: [web]
command: ["php", "/opt/lab/tools/monitor.php"]
Start the vulnerable build:
./run.sh vulnerable # PHP 8.4.26
Verifying exposure (and why there is no exploit here)
I did not reproduce the memory corruption, and the lab does not try. PHP says exploit reliability depends on the PHP build, extensions and allocator layout, and I had no safe, reliable way to demonstrate it. What the lab can show is the version, the configuration and the detection behaviour.
tools/check-version.sh compares a version with the fixed releases from the advisories:
# abridged
branch="${v%.*}"
case "$branch" in
8.5) fixed=8.5.12 ;; 8.4) fixed=8.4.27 ;;
8.3) fixed=8.3.36 ;; 8.2) fixed=8.2.35 ;;
*) echo "UNKNOWN: branch not in the advisory's fixed list"; exit 2 ;;
esac
lowest=$(printf '%s\n%s\n' "$v" "$fixed" | sort -V | head -1)
[ "$lowest" = "$fixed" ] && echo "PATCHED" || echo "VULNERABLE"
Running ./tools/verify.sh queries version.php, applies that check, and then tests the detection signal:

Two details matter. session.upload_progress.enabled=1 is PHP’s default, so a stock install has a feature the advisory names as an early consumer of stale state. And a version string is only a starting point: distributions backport fixes without changing it, so check your vendor’s advisory too.
Fixing it
Upgrade to a fixed release
The advisories say updating is the only complete fix. For CVE-2026-109618, the patch removes longjmp() for OOM. It uses a delayed soft OOM at the next VM safe point and an immediate hard OOM with _exit() at a higher threshold. Switch the lab to the fixed image:
./run.sh fixed # PHP 8.4.27
./tools/verify.sh
Interim hardening if you cannot patch yet
PHP’s advisories list settings that reduce exposure to specific paths. None is a complete fix. They must be set in php.ini, an FPM pool or Apache config, because ini_set() and .user.ini apply too late.
session.upload_progress.enabled = 0 ; blocks the confirmed session path for CVE-2026-109619
fatal_error_backtraces = 0 ; PHP 8.5 only: removes the confirmed RCE producer
log_errors = On ; a clean log is not proof of safety (the advisory says so)
error_reporting = E_ALL
post_max_size = 8M
max_input_nesting_level = 16
The first four settings come from the CVE-2026-109619 advisory; post_max_size and max_input_nesting_level come from the CVE-2026-109618 advisory. The values 8M and 16 are my lab choices, not PHP’s.
Why these help: CVE-2026-109619 needs a producer request and a consumer request on one worker. Disabling upload progress removes the demonstrated session path, and disabling backtraces removes the producer for the confirmed PHP 8.5 case. The advisory says the multipart limits reduce hard OOM and denial-of-service risk but do not replace updating. Neither setting touches the INI-diagnostics consumer, and PHP says there is no complete generic configuration mitigation for CVE-2026-109619.
More from the advisories:
memory_limit=-1, set administratively with every worker restarted, is the CVE-2026-109618 advisory’s primary configuration mitigation. It removes the predictable limit the confirmed exploit relies on, but it is incomplete, and unlimited memory has an obvious cost, so the lab file leaves it out.pm.max_requests = 1(PHP-FPM) limits worker reuse, at a performance cost. Recycling reduces success per request but does not remove the condition.- Do not use
RLIMIT_ASorRLIMIT_DATA: the resultingmmap()failures trigger the vulnerable path. - A cgroup
memory.maxis not a complete mitigation, and killing a cgroup can take down the master and every worker. - If you do not use multipart forms, block them at the frontend.
General hardening, not from the advisory: rate-limit and size-limit requests at your proxy or WAF, and run PHP containers as non-root on a read-only filesystem. These limit damage after a compromise.
Re-testing the fixes
Apply the hardening file to the vulnerable image and re-run the checks:
./run.sh hardened # 8.4.26 + conf/hardening.ini
./tools/verify.sh

The setting changed, but the verdict is still VULNERABLE, as it should be. Now the fixed image:

Note that session.upload_progress.enabled is still 1 on the patched image. The advisory suggests keeping it disabled after patching if you do not need it.
Vulnerable vs. fixed
| 8.4.26 (affected) | 8.4.26 + hardening | 8.4.27 (fixed) | |
|---|---|---|---|
| Version check | VULNERABLE | VULNERABLE | PATCHED |
session.upload_progress.enabled |
1 | 0 | 1 (default) |
| Root causes addressed | No | Some paths only | Yes, per PHP’s advisories |
| Worker child alert in the lab | Yes | Yes | Yes |
The last row is a lab artefact: the child is a simulation started by upload.php, so it fires on every version. It shows the monitor works, not that a patch changes it.
Detecting this class of bug
The signal Varonis calls high-confidence is a PHP worker creating a child process while it is still parsing an upload. A web worker normally runs PHP in-process, so a shell or other binary under a worker is rare, and on mod_php the corruption happens before application code runs. That is why application logs may show little.
The lab’s monitor reads /proc and alerts when a worker has a child that is not another worker:
foreach ($all as $cpid => $c) {
if ($c['ppid'] !== $pid) continue; // child of this worker
if (in_array($c['comm'], WORKER_COMMS, true)) continue; // the pool spawning workers is normal
fwrite(STDOUT, sprintf("%s ALERT worker pid=%d spawned child pid=%d comm=%s cmd=\"%s\"\n",
date('c'), $pid, $cpid, $c['comm'], $c['cmd']));
}
On the lab, the alert looks like this:
2026-11-04T10:25:13+00:00 ALERT worker pid=27 (apache2) spawned child pid=29 comm=sleep cmd="sleep 3"
On a real host, ps -eo pid,ppid,comm --forest gives a quick manual look at process trees. A 100 ms /proc poll can miss short-lived children, so for continuous coverage use the kernel audit log. This auditd rule records every execve made by the web user (Debian and Ubuntu run Apache as www-data; check yours with id www-data):
# /etc/audit/rules.d/php-worker-exec.rules (load with: augenrules --load)
-a always,exit -F arch=b64 -S execve -F uid=www-data -k php_worker_exec
Then search with ausearch -k php_worker_exec -i. Each record carries the process and its parent PID, so you can alert when the parent is an Apache or PHP-FPM worker. The rule is a starting point: legitimate code that calls mail() or exec() will match too, so baseline your hosts first. It works because a web worker normally runs PHP in-process and rarely calls execve, so a short, quiet list is easy to alert on. The rule syntax follows the auditctl man page; I did not run auditd in this lab. eBPF tools and EDR products can express the same "parent is the web worker" condition. Correlating with in-flight multipart requests is something this lab cannot do.
Other signals from the vendor write-ups:
- Unusual multipart requests followed by memory-limit errors or bursts of HTTP 500 responses.
- Request side: access-log entries for multipart POSTs followed by HTTP 500 responses, and repeated PHP fatal errors or warnings in the error log (the advisory asks you to audit these, while warning that a clean log does not prove safety).
- An oversized or malformed
PHPSESSIDin a response, which Varonis links to leaked process memory. - PHP process failures and unexpected command execution. Recorded Future suggests reviewing logs from 2026-10-20 onward, since attempts may predate the release.
These guide investigation and do not replace patching.
Key takeaways
- PHP2Shell is two PHP-core flaws (CVE-2026-109618, CVE-2026-109619), CVSS v4 9.2, fixed on 2026-10-21 in 8.5.12, 8.4.27, 8.3.36 and 8.2.35.
- Clean application code does not protect you. Patch the PHP runtime, including copies bundled in containers and appliances.
- Not confirmed here: the "back to 5.4" claim, the memory-read details, and any ASLR requirement. Treat them as reported, not established.
- The interim settings reduce specific paths.
memory_limit=-1,pm.max_requests=1andsession.upload_progress.enabled=0help in some cases, but PHP says only an upgrade fully mitigates the bugs. - Alert on
execvewhere the parent is a web worker, enable error logging, and rotate secrets if you suspect exposure.
Lab: download and run it
php2shell/
Dockerfile docker-compose.yml docker-compose.hardened.yml run.sh README.md
conf/hardening.ini
www/ index.php version.php upload.php (the only served directory)
tools/ monitor.php verify.sh check-version.sh (not served)
Prerequisites: Docker with Compose, bash and curl (Linux, macOS or WSL), free port 8080 on loopback.
unzip php2shell-lab.zip && cd php2shell
./run.sh vulnerable && ./tools/verify.sh # VULNERABLE PHP 8.4.26, then one ALERT
./run.sh hardened && ./tools/verify.sh # still VULNERABLE, upload_progress=0
./run.sh fixed && ./tools/verify.sh # PATCHED PHP 8.4.27
./run.sh down # stop and remove the containers
If your unzip tool dropped the executable bit (common on Windows and WSL), run the scripts through bash instead: bash run.sh vulnerable and bash tools/verify.sh. Do not expose port 8080 beyond loopback: upload.php starts a harmless sleep 3 for anyone who can reach it.
Watch alerts live with docker compose logs -f monitor. Remove the images afterwards with docker image rm php2shell-lab:8.4.26-apache php2shell-lab:8.4.27-apache. The lab does not trigger the vulnerabilities and has no RCE code; the "child process" is a harmless sleep 3.
References
- NVD: CVE-2026-109618 and CVE-2026-109619
- php-src advisory GHSA-w8hw-w9fx-j79w (RCE via OOM corruption)
- php-src advisory GHSA-6wmf-4rv5-p3rp (RCE via stale global state)
- Varonis: PHP2Shell
- Recorded Future: PHP2Shell vulnerability exploitation
- NCSC-NL: php2shell tracking wiki
- eness PoC repository wiki (reviewed; its technical description does not match PHP’s advisory)