Calif Research’s php2shell report examines two PHP remote-code-execution vulnerabilities, CVE-2026-109618 and CVE-2026-109619. PHP addressed them in its October 21, 2026 security release. The research was carried out by Calif with OpenAI and Wiz, and the write-up is credited to Ryan Austin of Calif.

The earliest affected path identified by the researchers dates to PHP 5.4.0. Exploitation depends on the PHP version and SAPI, but the report says the affected paths work under default configurations. The researchers confirmed pre-authentication RCE in WordPress 7.1.2 and Drupal 11.4.3. They also tested Joomla 6.1.4 and MediaWiki 1.46.2. Roundcube 1.7.4 and Magento 2.4.9 were affected in the reported tests. The list includes phpMyAdmin 5.2.3 and Nextcloud 35.0.1. Laravel 13.11 and Symfony 8.1 are named as well, and WHMCS is listed without a version. Some attack paths work against arbitrary PHP scripts.

The research builds on a long history of PHP memory-limit bugs. Stefan Esser reported the first PHP memory-limit remote vulnerability in 2004 and withheld a public exploit because of its severity. CORE and Immunity later developed non-public exploits. Fake exploit claims circulated separately on IRC. Calif says its earlier research into PHP use-after-free issues found no exploitable default vectors in the remaining unserializable classes.

In 2020, jr@coredu.edu reported PHP bug #78876 after observing that long variables could trigger an out-of-memory condition and leave temporary files behind. The report’s timeline attributes it to jr@coredu.mp. An OSTIF and Quarkslab audit rediscovered a related condition in 2024, but exploitation was not demonstrated then. A PHP patch related to #78876 had also mitigated the pattern in HHVM. Its Git history carries a `security-audit-2024` tag, even though the patch dates from 2020.

OpenAI brought Calif into the Patch the Planet initiative on July 30, 2026. Stefan Esser connected the researchers with Volker Dusch from the PHP Foundation’s ecosystem security team. Calif reported its first bug to PHP on August 11, shared an RCE proof of concept on August 14, and received confirmation on August 15. The second bug was reported on August 21. Ryan Austin had posted a hash for one finding on August 5. The patched releases followed on October 21.

The first exploitation route begins with memory pressure. PHP checks the declared Content-Length against `post_max_size` before parsing a POST body, but does not apply the same comparison to the bytes it actually consumes. Chunked HTTP/1.1 requests and lengthless HTTP/2 multipart requests can therefore deliver more data than the preliminary check suggests. An input-decompression filter creates a similar mismatch between compressed input and the decoded stream.

A second trigger uses multipart uploads with `filename=""`. PHP does not count these empty file parts against `max_file_uploads`, yet it creates six `$_FILES` fields for each one: `name`, `full_path`, `type`, `tmp_name`, `error` and `size`. Deeply nested field names force PHP to build separate hash-table structures for every field. Hundreds of short records can exceed the default 128 MiB `memory_limit`. The complete request still remains below the default 8 MiB `post_max_size`. A later allocation then produces a controlled fatal error.

The first flaw is in `_zend_hash_str_add_or_update_i`. PHP increments `nNumUsed` and `nNumOfElements` before allocating and assigning the bucket’s key. If that allocation triggers a fatal error, the bucket is already visible to teardown even though its key, hash and value were never initialized. Request-variable registration reaches this code through both URL-encoded POST parsing and RFC1867 multipart parsing, before the target script starts. During mod_php shutdown, PHP destroys the malformed hash table and interprets the stale bucket as valid data.

PHP-FPM handles the same startup failure differently. It finishes the FastCGI request, shuts down the PHP module and exits the worker with `FPM_EXIT_SOFTWARE`. The worker therefore does not retain the malformed request state. This makes the first chain an RCE path against Apache mod_php, with the report identifying arbitrary-script exploitation from PHP 7.3 onward for that route.

The researchers then generalized the pattern as state becoming visible before fallible work completes, followed by a fatal error that skips the corresponding rollback. An LLM-assisted review turned that idea into a `SKILL.md` search pattern and found a second path in Session upload progress. Multipart parsing can initialize Session before ordinary module activation. If parsing fails after `FILE_START`, the cleanup callback is skipped, `PG(modules_activated)` remains false, and Session’s globals retain pointers into the request heap after that heap is freed. A later multipart request carrying a Session ID and `PHP_SESSION_UPLOAD_PROGRESS`, but no file part, reaches cleanup without resetting the stale state. This gives mod_php a second-request use-after-free route. PHP-FPM exits on the startup failure, so that route does not survive there.

A separate chain reaches PHP-FPM after request startup has succeeded. The generic unserializer stores reference and destructor bookkeeping in Basic extension globals. A fatal error during application-level decoding can leave `BG(unserialize).data` pointing into freed request memory with a nonzero nesting level. The next request begins Session upload-progress handling before Basic’s normal initialization and can reclaim and reuse that storage. Serialized `R:` and `r:` references can then address selected entries in the stale reference list. A negative destructor-block `used_slots` value can select memory before the intended array; duplicate serialized keys reach `var_push_dtor_value()` and copy a genuine zval into the selected slot. The report describes this as application-dependent RCE in both mod_php and PHP-FPM, through operations such as `session_start()`, `session_decode()`, `unserialize()` and WordPress’s `maybe_unserialize()`.

Calif says no special fuzzing harness or unusually capable model was needed. The researchers relied on knowledge of PHP’s older vulnerability history and used LLMs to follow specific leads. Their conclusion is that general prompts do not reliably rediscover obscure, decades-old exploitation primitives.

The recommendation is to patch immediately using the PHP October 2026 security release. As temporary measures, PHP-FPM deployments can disable `session.upload_progress.enabled` and set `memory_limit=-1`; mod_php deployments can set `memory_limit=-1`. These settings do not replace an update. Removing the memory limit can increase the risk of process exhaustion.