Dissecting PHP2Shell (CVE-2026-109618 & CVE-2026-109619): Pre-Auth RCE in PHP Core, Mitigation & Patching
A deep-dive technical analysis of PHP2Shell (CVE-2026-109618 & CVE-2026-109619), the critical CVSS 9.2 pre-authentication RCE flaws in PHP RFC 1867 multipart parsing and session upload progress tracking, complete with C-level root cause, WAF rules, and patching playbooks.
When a vulnerability surfaces inside a popular WordPress plugin or a Laravel package, the blast radius is bounded by the sites running that specific component. When a pre-authentication memory corruption vulnerability surfaces inside the PHP core interpreter itself—specifically inside the request startup pipeline that runs before your application script even begins executing—the blast radius encompasses virtually every unpatched PHP workload on the internet.
Tracked collectively as PHP2Shell, CVE-2026-109618 and CVE-2026-109619 represent one of the most consequential PHP runtime disclosures since the CGI argument injection bug (CVE-2012-1823) and the Windows iconv bug (CVE-2024-4577). Rated 9.2 (Critical) on the CVSS v4.0 scale, the flaws reside in the intersection of PHP's RFC 1867 multipart/form-data stream parser (main/rfc1867.c), the session upload progress subsystem (ext/session/session.c), and the Zend Engine's error-state lifecycle.
Following a premature leak on social media and underground forums ahead of coordinated disclosure, the PHP Foundation issued an advance emergency warning and released out-of-cycle patches on October 21, 2026, at 14:00 UTC. In this technical deep dive, we dissect the C-level root cause of PHP2Shell, explain why early configuration workarounds were walked back, and provide a comprehensive defense-in-depth playbook for patching, virtual patching, and threat hunting.
1. Why PHP2Shell Is So Dangerous: Pre-Script Execution in RINIT
A common misconception among web developers is: "My application doesn't use PHP sessions or accept file uploads, so I'm not vulnerable."
That assumption is false. To understand why, we must examine the internal PHP Server API (SAPI) request lifecycle. When an HTTP request arrives at Nginx + PHP-FPM or Apache + mod_php targeting any .php endpoint (even a 1-line healthcheck.php that returns 200 OK), the interpreter executes several phases before compiling or running the target script:
HTTP Request (Content-Type: multipart/form-data)
│
├─► 1. php_request_startup() [RINIT Phase]
│ ├─► sapi_activate()
│ │ └─► sapi_read_post_data()
│ │ └─► rfc1867_post_handler() [main/rfc1867.c]
│ │ │
│ │ ├─► Parses multipart boundaries & chunks
│ │ └─► Invokes php_rfc1867_callback()
│ │ └─► php_session_rfc1867_callback() [ext/session/session.c]
│ │ └─► ⚠️ VULNERABILITY TRIGGERED HERE
│ │
│ └─► zend_activate_modules()
│
└─► 2. php_execute_script() [Userland PHP code — NEVER REACHED IF EXPLOITED]Because RFC 1867 multipart/form-data parsing occurs inside sapi_activate() during the request initialization (RINIT) phase, PHP processes the multipart body and invokes registered upload hooks before userland code, framework middleware, authentication guards, or CSRF checks ever execute. Any publicly reachable .php route can serve as the entry point.
2. Technical Root Cause: Dissecting CVE-2026-109618 & CVE-2026-109619
PHP2Shell is not a single bug, but a pair of tightly coupled vulnerabilities in PHP's core C codebase that together enable reliable, unauthenticated Remote Code Execution:
- CVE-2026-109618: Memory exhaustion and state manipulation in RFC 1867 multipart stream parsing (
main/rfc1867.c) intersecting with session upload tracking. - CVE-2026-109619: Heap Use-After-Free (UAF) via dangling
zval *pointers inext/session/session.cand unsafe global state management following fatal error/bailout conditions.
The Anatomy of the Dangling Pointer in ext/session/session.c
Since PHP 5.4, the session.upload_progress feature has been enabled by default (session.upload_progress.enabled = 1). When an incoming multipart/form-data request contains a field matching session.upload_progress.name (by default, PHP_SESSION_UPLOAD_PROGRESS), rfc1867_post_handler() fires a sequence of lifecycle events into php_session_rfc1867_callback():
MULTIPART_EVENT_START: Initializes the tracking structure (php_session_rfc1867_progress).MULTIPART_EVENT_FORMDATA: Captures thePHP_SESSION_UPLOAD_PROGRESSkey and initializes the session if a session ID is present.MULTIPART_EVENT_FILE_START: Populates the file metadata array inside$_SESSION["upload_progress_..."].MULTIPART_EVENT_FILE_DATA: Updates byte counters as chunks stream in.MULTIPART_EVENT_FILE_END&MULTIPART_EVENT_END: Finalizes and flushes the session state.
To optimize performance during multi-chunk uploads, the vulnerable implementation of php_session_rfc1867_progress cached raw C pointers (zval *) pointing directly into the internal storage buckets (Bucket elements inside arData) of the dynamic $_SESSION HashTable:
/* Vulnerable structure pattern in ext/session/php_session.h (Pre-Patch) */
typedef struct _php_session_rfc1867_progress {
size_t sname_len;
zend_string *key;
zval sid;
zval data;
zval files;
zval *post_bytes_processed; /* ⚠️ Raw pointer into HashTable bucket! */
zval *current_file;
zval *current_file_bytes_processed; /* ⚠️ Raw pointer into HashTable bucket! */
/* ... */
} php_session_rfc1867_progress;In C and the Zend Engine, caching a raw zval * pointer into a mutable HashTable across complex I/O boundaries violates a cardinal rule of Zend memory safety: a HashTable is free to reallocate, resize, or free its underlying arData buffer whenever elements are inserted, rehashed, or cleaned up.
HashTable memory is freed or moved on the Zend heap (efree / erealloc). However, progress->post_bytes_processed and progress->current_file_bytes_processed are never invalidated. They become dangling pointers pointing into freed Zend heap chunks.From Dangling Pointer to Arbitrary Code Execution
Once those pointers are dangling on the Zend Memory Manager (ZMM) heap, the attacker controls what happens next:
- Heap Feng Shui / Reallocation: Subsequent multipart fields or headers within the same HTTP stream allocate new Zend structures (such as
zend_string,zend_array, orzend_objectinstances) that reclaim the exact freed heap slot still referenced byprogress->post_bytes_processed. - Controlled Write-What-Where via Progress Updates: As additional file data chunks arrive (triggering
MULTIPART_EVENT_FILE_DATA), the callback executes:Z_LVAL_P(progress->post_bytes_processed) = data->post_bytes_processed;
Because the attacker controls the exact number of bytes transmitted in the multipart body, the interpreter writes an attacker-controlled 64-bit integer directly over the reclaimed structure's fields! - Fatal Error State Corruption (CVE-2026-109619): By chaining this corruption with forced engine error/bailout states (
zend_bailout) or corruptingzvaltype tags (e.g., flipping a primitive value into an object/callable pointer or corrupting a destructor handlerpDestructorinside azend_array), the cleanup routine at the end of the request executes an attacker-controlled function pointer (such assystem/zend_call_function) with controlled arguments.
3. How the Official Patch Fixes the Flaw
In the emergency security releases shipped on October 21, 2026, the PHP core team fundamentally redesigned how ext/session/session.c tracks upload progress.
Rather than holding raw zval * pointers into mutable HashTable buckets across multipart events, the patched code stores byte counts as plain value-type integers (zend_long) directly inside the php_session_rfc1867_progress struct and synchronizes them into the HashTable via a dedicated helper function (php_session_rfc1867_sync_counters()) only when flushing state:
/* Patched design pattern in ext/session/php_session.h & session.c */
typedef struct _php_session_rfc1867_progress {
/* Raw zval* pointers removed; replaced with value primitives */
zend_long post_bytes_processed;
zend_long current_file_bytes_processed;
/* ... */
} php_session_rfc1867_progress;
static void php_session_rfc1867_sync_counters(php_session_rfc1867_progress *progress) {
/* Safely looks up and updates the HashTable keys by value on demand,
eliminating dangling pointer dereferences across chunk boundaries */
if (Z_TYPE(progress->data) == IS_ARRAY) {
add_assoc_long_ex(&progress->data, "bytes_processed",
sizeof("bytes_processed") - 1, progress->post_bytes_processed);
}
if (Z_TYPE(progress->current_file) == IS_ARRAY) {
add_assoc_long_ex(&progress->current_file, "bytes_processed",
sizeof("bytes_processed") - 1, progress->current_file_bytes_processed);
}
}Additionally, the patches harden RFC 1867 boundary/memory handling (CVE-2026-109618) and ensure global interpreter state is cleanly torn down when fatal errors occur mid-request (CVE-2026-109619).
4. Official Patched Versions & Branch Matrix
The PHP Foundation only issues official source and binary patches for actively supported PHP 8.x branches. If you are running an End-of-Life (EOL) version of PHP (PHP 8.1, 8.0, 7.x, or 5.x), upstream PHP will not release an official patch for your branch—you must either upgrade to a supported branch, rely on enterprise OS vendor backports (e.g., Ubuntu Pro, Debian LTS, RHEL), or apply strict edge mitigations immediately.
| PHP Branch | Support Status | Vulnerable Versions | Patched Version |
|---|---|---|---|
| PHP 8.5 | Active Support | < 8.5.12 | 8.5.12+ |
| PHP 8.4 | Active Support | < 8.4.27 | 8.4.27+ |
| PHP 8.3 | Security Support | < 8.3.36 | 8.3.36+ |
| PHP 8.2 | Security Support | < 8.2.35 | 8.2.35+ |
| PHP 8.1 / 8.0 / 7.x / 5.x | End-of-Life (EOL) | All 5.4+ releases | No upstream patch (Upgrade or Distro Backport Required) |
Step-by-Step Patching Commands
1. Ubuntu / Debian (using ppa:ondrej/php or Sury repositories):
sudo apt update
sudo apt install --only-upgrade "php*"
# Crucial: Restart PHP-FPM or Apache so worker processes load the new binary!
sudo systemctl restart php8.4-fpm # Adjust for your PHP version (8.2, 8.3, 8.4, 8.5)
sudo systemctl restart apache2 # If using mod_php2. RHEL / AlmaLinux / Rocky Linux / Fedora (Remi Repository):
sudo dnf clean all
sudo dnf upgrade "php*" -y
sudo systemctl restart php-fpm httpd3. Docker & Kubernetes Workloads:
Pull the latest upstream image tags (php:8.4.27-fpm-alpine, php:8.3.36-fpm-bookworm, etc.), rebuild your application containers with --no-cache --pull, and roll your deployments:
docker build --pull --no-cache -t myapp:patched .
kubectl rollout restart deployment/myapp-php5. Why session.upload_progress.enabled = 0 Is Not Enough on Its Own
In the hours following the initial leak, many advisories recommended setting session.upload_progress.enabled = 0 in php.ini as a silver-bullet fix. Security teams have since walked back that claim as a standalone guarantee for three critical reasons:
- Two Distinct CVEs: While disabling
session.upload_progress.enabledshuts down the primaryphp_session_rfc1867_callbackUAF path, CVE-2026-109618 and CVE-2026-109619 also involve core RFC 1867 multipart parsing and post-fatal-error global state corruption that can be reached independent of the upload progress toggle. - Per-Directory Overrides (
INI_PERDIR): In PHP,session.upload_progress.enabledhasPHP_INI_PERDIRpermissions. If an attacker or shared-hosting tenant can influence a.user.inior.htaccessfile—or if an application sets it at runtime in specific SAPI contexts—a simplephp.inichange can be bypassed unless locked viaphp_admin_flag. - Unrestarted Workers: Editing
php.inidoes nothing for long-livedphp-fpmorFrankenPHP/ Swoole worker processes until the master daemon is reloaded.
6. Defense-in-Depth Mitigations & Virtual Patching
If you are managing legacy applications that cannot be upgraded immediately—or want layered protection in front of patched servers—apply the following defense-in-depth controls:
A. Lock Down PHP-FPM Pool & php.ini Settings
Use php_admin_flag in your PHP-FPM pool configuration (e.g., /etc/php/8.4/fpm/pool.d/www.conf) so that .user.ini files cannot re-enable upload progress tracking, and tighten multipart resource limits:
; In /etc/php/8.x/fpm/pool.d/www.conf
php_admin_flag[session.upload_progress.enabled] = off
php_admin_flag[session.upload_progress.cleanup] = on
; In php.ini — restrict multipart exhaustion & dangerous execution primitives
session.upload_progress.enabled = 0
max_file_uploads = 5
max_input_time = 15
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_execB. Nginx Reverse Proxy Mitigation (Block Unsolicited Multipart & Upload Progress)
If your application does not accept public file uploads—or only accepts uploads on authenticated routes—configure Nginx to reject unauthenticated multipart/form-data requests and drop slow-stall upload streams:
# 1. Tighten body timeouts to defeat mid-stream UAF stall timing
client_body_timeout 10s;
client_header_timeout 10s;
# 2. If your site does NOT use public file uploads, block multipart/form-data outright:
if ($content_type ~* "^multipart/form-data") {
return 403;
}If your site does require file uploads (e.g., WordPress media uploads or user avatar endpoints), restrict multipart/form-data strictly to those specific URIs and block all requests carrying the PHP_SESSION_UPLOAD_PROGRESS field.
C. WAF / Coraza / ModSecurity SecRules
Deploy the following ModSecurity / Coraza rules at your edge or inside an inline IPS (such as Centinela) to block requests attempting to invoke PHP_SESSION_UPLOAD_PROGRESS or abuse anomalous multipart boundaries:
# Block any multipart form field or query parameter referencing PHP_SESSION_UPLOAD_PROGRESS
SecRule ARGS_NAMES|FILES_NAMES|MULTIPART_PART_HEADERS "(?i)PHP_SESSION_UPLOAD_PROGRESS" \
"id:1009618,phase:2,deny,status:403,log,t:none,t:urlDecodeUni,msg:'PHP2Shell (CVE-2026-109618/109619) Session Upload Progress Exploit Attempt'"
# Block multipart requests with strict structural anomalies
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
"id:1009619,phase:2,deny,status:403,log,t:none,msg:'Multipart Strict Parsing Error - Potential CVE-2026-109618 Exploit'"7. Post-Patch Threat Hunting & Incident Response
Because PHP2Shell was leaked publicly before the October 21 patch release, opportunistic mass-scanning began almost immediately. Patching closes the vulnerability, but it does not evict an attacker who already dropped a web shell or persistence mechanism.
Run the following threat-hunting checks across all internet-facing PHP hosts:
Audit Kernel Logs for PHP-FPM Segmentation Faults:Failed heap UAF attempts almost always crash the target php-fpm child worker with SIGSEGV (signal 11) before an attacker dials in the exact heap offsets:
dmesg -T | grep -i -E "php-fpm|segfault"
journalctl -u "php*-fpm" --since "7 days ago" | grep -E "SIGSEGV|SIGABRT|exited on signal"Check Web Server Access Logs for Anomalous POST Requests:Look for POST requests to unusual endpoints (like index.php, wp-login.php, or static-looking PHP scripts) accompanied by 500, 502 (worker segfault during heap layout probing), or 499 (deliberate client disconnects mid-upload) status codes:
grep -E 'POST .* HTTP/1\.[01]" (500|502|499|408)' /var/log/nginx/access.logHunt for Newly Created or Modified PHP Files in Web Roots:
find /var/www -type f -name "*.php" -mtime -7 -lsInspect Process Trees for Anomalous Children of PHP-FPM / Apache:
ps auxf | grep -E -A 5 "php-fpm|apache2|httpd"
# Look for sh, bash, curl, wget, python3, perl, nc, or base64 spawned under www-dataSummary
PHP2Shell (CVE-2026-109618 and CVE-2026-109619) is a stark reminder that legacy C features embedded deep inside the HTTP request startup lifecycle represent prime real estate for memory corruption research. Because rfc1867.c and ext/session/session.c process multipart streams before userland PHP code even begins executing, no application-level framework or authentication middleware can save an unpatched runtime.
Upgrade your PHP interpreters to 8.5.12, 8.4.27, 8.3.36, or 8.2.35 immediately, lock down session.upload_progress.enabled via php_admin_flag, filter unsolicited multipart/form-data traffic at the edge, and audit your logs for worker crashes or post-exploitation artifacts.