CVE-2026-109618 and CVE-2026-109619 are two critical pre-authentication remote code execution vulnerabilities in the PHP core, impacting versions 5.4.0 through 8.5.11. Unlike most PHP vulnerabilities that reside in extensions or application code, php2shell lives in the interpreter itself, meaning an unauthenticated attacker can exploit a default PHP installation with no plugins, no special configuration, and no application-level authentication required. PHP runs on approximately 70% of websites, including platforms such as WordPress, Drupal and Wikipedia's MediaWiki, making it one of the most serious security threats in the PHP ecosystem.
Discovered by Calif, OpenAI and Wiz through the Patch the Planet initiative, these vulnerabilities were addressed in the PHP October 2026 Security Release. Together they represent a story that stretches back over two decades, from Stefan Esser's original 2004 memory-limit RCE, through a dormant 2020 bug report, a 2024 audit that found a related issue but couldn't prove exploitation, and finally to this year's breakthrough that surfaced multiple independent exploit paths.
PHP can leave dangling pointers and stale state when an allocation would exceed memory_limit, or when another fatal error interrupts native code before its normal cleanup completes. Request shutdown may then free memory that surviving state still references.
To reach an exploitable state, the researchers found two general OOM/memory-limit based fatal producers and three different interruptible points in the interpreter that can leave a stale or consumable state, allowing memory leakage and remote code execution.
The first step is causing a controlled out-of-memory condition. PHP compares post_max_size with the declared Content-Length before automatic POST parsing begins, but it does not enforce the same limit against the bytes it actually consumes. Apache can therefore pass a chunked HTTP/1.1 or lengthless HTTP/2 multipart request with no meaningful declared length. An input-decompression filter creates the same mismatch between the compressed length and the decoded stream.
The second piece is exhausting memory_limit with a multipart request that remains below post_max_size. An RFC1867 file-upload part with filename="" contains no file data, and PHP does not count it toward max_file_uploads. PHP still creates all six corresponding $_FILES entries: name, full_path, type, tmp_name, error, and size. When the form field name is nested, such as files[a][b][c]..., PHP must create that hierarchy separately for each of the six entries. One short empty-upload record expands into many native HashTables, Buckets and string-key allocations. Hundreds of these records can occupy more than the default 128 MiB PHP heap while their complete HTTP body remains below the default 8 MiB post_max_size. This lets the attacker make a chosen later allocation hit memory_limit on demand.
Now that an OOM can be triggered on demand, the question is what can be interrupted to leave the process in an exploitable state. The researchers found several such points.
The first was discovered in _zend_hash_str_add_or_update_i, where PHP increments both nNumUsed and nNumOfElements before fully initializing the new bucket. Increasing nNumUsed makes the bucket visible to teardown, even if the subsequent key allocation fatals. This allows the attacker to ensure that local control flow never reaches the key, hash, zval or collision-chain assignments, but the count remains incremented. Teardown later consumes stale allocation bytes as a live bucket.
This path is reached through normal request-variable registration during php_request_startup(). Both URL-encoded POST and RFC1867 upload paths execute before the target PHP file runs. When the key allocation fatals, the SAPI determines whether the partial bucket is ever consumed:
In mod_php, registered request cleanup runs even after startup failure, reaching php_request_shutdown(NULL) which destroys the malformed $_POST`/`$_FILES, while the Apache worker survives with stale state. In PHP-FPM, startup failure takes the process-exit branch with php_module_shutdown(), discarding the stale state entirely so the worker does not survive.
This results in RCE against arbitrary PHP scripts on Apache mod_php and PHP >= 7.3.
The second interruption point was found in Session upload-progress. This feature can open and update a session while PHP is still parsing the request body, before application code executes. Multipart parsing can enter the session extension before PHP considers its modules activated. In php_session_rfc1867_callback(), the first qualifying file part takes a path that calls php_rinit_session(0) before normal module activation completes.
The matching cleanup is vulnerable to interruption. Ordinarily, the multipart END callback cleans up this early session state. If parsing fails first, that callback is skipped. Request shutdown also gates extension shutdown on the activation flag:
/* php_request_shutdown() */
if (PG(modules_activated)) {
zend_deactivate_modules();
}On the affected mod_php path, PG(modules_activated) is still false, so session RSHUTDOWN does not run. The request heap is destroyed regardless. The Apache child can now handle another request with PS(id) and PS(http_session_vars) still pointing into the freed heap. PHP-FPM again discards the state because the worker exits on startup failure.
This results in RCE against arbitrary PHP scripts on Apache mod_php and PHP >= 5.4.0.
The third and most dangerous path affects PHP-FPM, which is the deployment model used by many modern PHP applications. PHP-FPM terminates its worker when php_request_startup() fails, but once startup has succeeded, an ordinary fatal during script execution follows the normal request-shutdown path, and the worker can accept another request.
The researchers found that PHP's generic unserializer keeps shared bookkeeping for serialized references and values awaiting destruction. php_var_unserialize_init() allocates this state from the request heap and publishes it in the Basic extension's globals. A fatal inside the live decoder can skip the cleanup operation. The Session decoder provides a straightforward example:
PHP_VAR_UNSERIALIZE_INIT(var_hash);
/* Decode the Session's values. */
current = var_tmp_var(&var_hash);
if (php_var_unserialize(current,
(const unsigned char **)&q,
(const unsigned char *)endptr,
&var_hash)) {
/* ... */
}
/* ... */
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);In phpMyAdmin, a normal session_start() loads a Session record built through prior HTTP requests. WordPress reaches the same generic decoder through ordinary maybe_unserialize() calls. By controlling the memory pressure around those operations, the attacker can let the decoder build useful reference and destructor lists before a later allocation hits memory_limit.
The outer Session error handler cleans up Session, but it does not clear Basic's abandoned decoder state. Request shutdown releases the heap while BG(unserialize).data still points into it and the nesting level remains nonzero. Because this fatal happened after successful request startup, PHP-FPM keeps the worker alive.
The next multipart request reaches Session upload progress before Basic's normal request initialization. The stale nesting level makes php_var_unserialize_init() treat the dangling pointer as an active nested decoder. Ordinary request allocations can reclaim the freed storage before that happens.
Serialized R: and r: references can follow selected entries in the reclaimed reference list. The destructor list gives a second primitive: a controlled native zval copy. Each destructor block has a signed used_slots count. Its allocation helper checks whether adding another entry would exceed the block's capacity, but assumes the existing count cannot be negative. After reclamation, the attacker can violate that assumption. On the tested 64-bit layout, the selected slot is calculated as:
slot = destructor_block + 16 + 16 * used_slotsA negative count selects memory before the intended array. Duplicate keys in the serialized input then reach var_push_dtor_value(), which preserves the previous value by copying its genuine zval into that selected slot. The attacker therefore controls where PHP places a pointer and its associated type information. This is application-dependent RCE under both Apache mod_php and PHP-FPM.
The earliest affected path dates to PHP 5.4.0. The available exploitation paths depend on the PHP version and SAPI, but are always exploitable under default configurations. The researchers confirmed pre-auth RCE on the following applications: WordPress 7.1.2, Drupal 11.4.3, Joomla 6.1.4, MediaWiki 1.46.2, Roundcube 1.7.4, Magento 2.4.9, phpMyAdmin 5.2.3, Nextcloud 35.0.1, Laravel 13.11, Symfony 8.1, and WHMCS.
The history of these vulnerabilities reads like a cautionary tale about dormant security issues: 2004 Stefan Esser discovered the first PHP memory-limit remote vulnerability, critical enough that he would not release his exploit publicly. 2020 Bug #78876 was reported to the PHP project, noting that certain long variables could cause out-of-memory conditions and failure to clean up temporary files. At the time, not much was made of the report. 2024 OSTIF and Quarkslab performed an audit of PHP and rediscovered a related OOM issue, but exploitation was not proven, hence the bug was not fixed. 2026, August 1 Calif joined OpenAI's Patch the Planet initiative and began auditing PHP. Within days, they surfaced the first OOM-fatal and the first of the three interruption points. 2026, October 21 Patched versions were released.
In order to fix the disclosed vulnerabilities, please refer to the PHP October 2026 Security Release for official recommendations. The following versions are considered patched and safe to use: PHP 8.5.12, PHP 8.4.27, PHP 8.3.36, and PHP 8.2.35. Versions prior to 5.4.0 are not affected. It is recommended that you update your deployments immediately.
If you cannot patch immediately, the following temporary, partial mitigations may buy you some time: for PHP-FPM, disable session upload progress with session.upload_progress.enabled=0 and set memory_limit=-1. For mod_php, set memory_limit=-1. These changes do not replace a patch. Setting memory_limit=-1 removes PHP's memory cap, increasing the risk of memory exhaustion.