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.

Dissecting PHP2Shell CVE-2026-109618 and CVE-2026-109619 Pre-Auth RCE in PHP Core
🚨
Critical Threat Advisory: PHP2Shell (CVE-2026-109618 & CVE-2026-109619) A pair of critical pre-authentication Remote Code Execution (RCE) vulnerabilities (CVSS v4.0: 9.2) in the PHP core engine allows unauthenticated attackers to corrupt heap memory and execute arbitrary code before a single line of application PHP code runs. Emergency patches shipped on October 21, 2026 for PHP 8.5.12, 8.4.27, 8.3.36, and 8.2.35.

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 in ext/session/session.c and 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():

  1. MULTIPART_EVENT_START: Initializes the tracking structure (php_session_rfc1867_progress).
  2. MULTIPART_EVENT_FORMDATA: Captures the PHP_SESSION_UPLOAD_PROGRESS key and initializes the session if a session ID is present.
  3. MULTIPART_EVENT_FILE_START: Populates the file metadata array inside $_SESSION["upload_progress_..."].
  4. MULTIPART_EVENT_FILE_DATA: Updates byte counters as chunks stream in.
  5. 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.

🔬
The Memory Corruption Primitive (Use-After-Free) When a crafted multipart stream forces a session state re-initialization, table reallocation, or mid-stream error/stall condition between multipart events, the underlying 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:

  1. Heap Feng Shui / Reallocation: Subsequent multipart fields or headers within the same HTTP stream allocate new Zend structures (such as zend_string, zend_array, or zend_object instances) that reclaim the exact freed heap slot still referenced by progress->post_bytes_processed.
  2. 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!
  3. Fatal Error State Corruption (CVE-2026-109619): By chaining this corruption with forced engine error/bailout states (zend_bailout) or corrupting zval type tags (e.g., flipping a primitive value into an object/callable pointer or corrupting a destructor handler pDestructor inside a zend_array), the cleanup routine at the end of the request executes an attacker-controlled function pointer (such as system / 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_php

2. RHEL / AlmaLinux / Rocky Linux / Fedora (Remi Repository):

sudo dnf clean all
sudo dnf upgrade "php*" -y
sudo systemctl restart php-fpm httpd

3. 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-php

5. 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:

  1. Two Distinct CVEs: While disabling session.upload_progress.enabled shuts down the primary php_session_rfc1867_callback UAF 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.
  2. Per-Directory Overrides (INI_PERDIR): In PHP, session.upload_progress.enabled has PHP_INI_PERDIR permissions. If an attacker or shared-hosting tenant can influence a .user.ini or .htaccess file—or if an application sets it at runtime in specific SAPI contexts—a simple php.ini change can be bypassed unless locked via php_admin_flag.
  3. Unrestarted Workers: Editing php.ini does nothing for long-lived php-fpm or FrankenPHP / 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_exec

B. 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.log

Hunt for Newly Created or Modified PHP Files in Web Roots:

find /var/www -type f -name "*.php" -mtime -7 -ls

Inspect 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-data

Summary

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.

Read more