The request reaches PHP before your controller can help
Your application can reject an upload after PHP has already processed part of its body. That ordering is the reason a runtime vulnerability deserves a different investigation from an unsafe upload controller. For CVE-2026-109618 and CVE-2026-109619, discussed publicly as PHP2Shell, the central question is where attacker-controlled request data crosses a boundary inside the interpreter.
I approach this incident through three layers: the confirmed security release, the runtime behavior visible in source, and the mechanism proposed by the reviewed implementations. This assessment is dated 21 October 2026 and uses static analysis of PHP 8.4.4, relevant PHP 8.5.4 code, proof-of-concept implementations reviewed for this analysis, reporting, and a reviewed demonstration. No independent exploitation was performed.
What PHP has confirmed
PHP’s 20 October notice confirms two critical vulnerabilities, each rated CVSS v4.0 9.2, with a security release scheduled for 21 October at 14:00 UTC. The retrieved advance notice reserves affected versions, fixed versions, and full technical details for that release. It does not itself identify the CVE numbers or upload progress as the root cause. The two identifiers and RCE description appear in the associated public reporting. [1, 2]
The Huntress report attributes reproduction of CVE-2026-109618 to watchTowr, while stating that Huntress had not reproduced the issues or observed related attacks in its partner environments as of 20 October. The reviewed demonstration shows effects consistent with command execution and file creation, including on WordPress and Drupal. The demonstration’s implementation has not been matched to the implementations reviewed here. These observations justify urgency while leaving the exact cause and deployment prerequisites to establish. [2]
Why upload progress belongs in the investigation
PHP can maintain session-backed progress information while parsing a multipart upload. The relevant state includes a request-wide byte counter, per-file counters, metadata, and completion flags. The feature works inside request processing, so a site’s visible upload form does not fully describe its runtime attack surface. PHP’s manual documents the feature and its configurable update frequency. [3]
In the reviewed source, FILE_START establishes file state, FILE_DATA updates counters, and END completes or cleans up tracking. The total-byte pointer is cached during initial construction; the current-file pointer is refreshed for each file. Counter writes precede the session-update helper. That order matters because authentication inside application code cannot automatically be assumed to protect earlier parsing. Access control at a proxy before PHP receives the request enforces a different boundary. [4]
The proposed chain, stage by stage
One reviewed proof of concept associates the first identifier with the total-byte counter; another associates the second with the current-file counter. Both propose a stale-pointer problem during repeated progress updates. These are the implementations’ proposed mappings, not mappings established by the retrieved vendor notice. Their causal model requires request reachability, creation of progress state, an allocation being freed before a later access, and useful control over the resulting corrupted state.
Turning that state into RCE also requires a reachable execution path and build-compatible addresses and structures. The demonstration displays an address-disclosure stage; the local framework’s symbol extraction does not establish an equivalent remote disclosure. Existing heap allocations do not prove later reuse of a particular freed slot, and two delayed socket writes do not prove two matching server callbacks. These are separate technical conditions. A successful chain has to connect them; the reviewed implementations leave that connection incomplete.
The lifetime question: who still owns the array?
A use-after-free means accessing an allocation after its lifetime has ended. Call the live progress array P and a separately decoded session array D. Equivalent contents do not make them the same allocation. On the ordinary successful update path in PHP 8.4.4, the helper increments the reference count before placing P into the session tree. Resetting that tree drops its reference, while the progress state still owns P. Replacing decoded D does not free P. The corresponding increment also exists in the reviewed PHP 8.5.4 helper. [4, 5, 6, 7]
This ownership model is the unresolved step in the proposed explanation. It does not clear either CVE or refute the separate demonstration. A complete root-cause account must identify a different path or condition that defeats the lifetime guarantee. Even end-of-request separation needs ordering analysis: with cleanup disabled, the old session tree still owns the original array at the final counter write, before the forced update and teardown. A pointer becoming stale alone is insufficient without a subsequent access. [4]
Session poisoning crosses a different boundary
A reviewed third-party lab report describes a second route to execution: attacker-controlled session content reaching an application file-inclusion sink. The deliberately vulnerable test endpoint passes a request parameter directly to include(). The persistence profile disables progress cleanup and changes update timing and multipart limits. The lab report records execution under these conditions. This demonstrates the kind of application boundary to audit, but it does not independently establish either CVE’s memory-corruption mechanism.
The same report reports session-ID selection without a Cookie header, session fixation in a deliberately permissive login fixture, and a serialization failure associated with a pipe character in a progress key. These observations depend on the handler, serializer, session policy, and application behavior. Session content must remain data; include/require paths should come from a controlled allowlist, and authentication transitions should replace the session identifier. A progress record alone does not demonstrate a freed allocation being reused.
The transcript’s uid=0 result describes the identity of its lab process. The launcher starts the PHP development server without specifying a reduced user; that output does not demonstrate privilege escalation or host-root access. This separation matters operationally: fixing an unsafe include sink and session policy addresses application-level exposure, while a runtime memory-safety flaw still requires its own vendor fix.
Affected versions and exploit compatibility are different questions
The retrieved vendor notice does not provide a confirmed affected-version range or fixed-version matrix. Broad claims covering PHP 5 onward remain claims in public reporting. PHP 8.4.4 is our source-analysis baseline; PHP 8.5.4 appears in the reviewed material and a displayed demonstration profile. Neither observation establishes the full affected range. Treat this as an evidence snapshot, and use the detailed vendor advisory for package decisions. [1, 8]
A vulnerability may span several branches while a particular exploit supports one architecture, package build, or server integration. Conversely, a failing PoC does not establish that a deployment is safe. The demonstration includes profiles described as Apache prefork with mod_php and PHP-FPM; it does not establish universal SAPI coverage. Inventory the actual serving build and integration, rather than inferring exposure from the CMS version or a CLI binary alone.
The reviewed third-party experiment report reports no command execution in its memory-corruption attempts on php:8.4.4-cli, php:8.4.4-fpm, and php:8.2.34-fpm, despite locally resolved addresses. These are the report author’s results, not tests performed for this article. They neither establish safe versions nor justify extending the successful include-sink example to default PHP installations. The author also proposes a CVSS v3.1 score; that is a separate author assessment, not a conversion of PHP’s vendor-issued v4.0 rating.
Which settings change the conditions?
For the proposed path, session.upload_progress.enabled controls tracking; name selects the triggering field; freq and min_freq control update thresholds; cleanup controls removal of progress information after POST processing. Session handlers and serialization influence persistence. These settings identify what to inspect, rather than a confirmed prerequisite list for both CVEs. In particular, eventual cleanup cannot be assumed to prevent an earlier memory error. [3, 9]
The SAPI, server buffering, request-body limits, and timeouts affect when data reaches the parser. A pause at the client may not survive buffering as a pause between PHP callbacks. Inspect effective configuration in the web-serving process and each FPM pool; CLI configuration can differ. If upload progress is unused, evaluate disabling it as precautionary hardening. The evidence reviewed here does not establish that change as a complete workaround for either CVE. [3, 9]
The reviewed lab configuration also exposes a configuration detail worth checking: session.upload_progress.freq=1 is an absolute byte interval, while 1% is relative to request size. Its memory-corruption test profile uses the former even though comments describe stock timing. Its persistence test profile sets cleanup=0 and min_freq=0. Compare effective values, including units, before generalizing results between these fixtures and production. [4, 9]
The blast radius follows the worker’s permissions
Execution under www-data can already expose an application. Depending on permissions, the worker may read database credentials, modify writable content, or reach backend services. Shared service identities and writable directories can extend the impact across applications. These are consequences to assess against the deployment, not claims that every effect occurred in the demonstration. The useful question is what the process can read, change, and reach.
Huntress recommends watching unexpected child processes from PHP and web-server workers. Correlate process ancestry with requests, timestamps, new files, and outbound connections; legitimate applications also launch tools, and execution can occur without a new shell. The reviewed trigger’s stale-file checks and the framework’s incomplete FastCGI response handling further illustrate why a tool verdict needs attribution. Huntress’s earlier PHP incident example was explicitly unrelated to PHP2Shell and should not be relabeled as exploitation of these CVEs. [2]
Verify the fix where requests actually run
Map public endpoints to their serving PHP packages or images, SAPIs, extensions, and effective configuration. Include vendor software and hosting environments that bundle PHP. Match the detailed upstream advisory to distribution or vendor packages, accounting for backported fixes. Replace or restart affected workers, then confirm that the corrected build handles traffic. For installations without maintained security updates, establish an upgrade or exposure-reduction plan.
The technical objective is to connect the vulnerable operation, its reachable conditions, and the code that restores the required invariant. A clean result from these proof-of-concept implementations cannot substitute for that mapping. My remediation-verification approach follows the claim into the deployed process and checks that the relevant fix is actually in service. If you need that assessment for your PHP estate, the work starts with the running environment and its evidence.
Sources
- [1] PHP: security-release advance notice
- [2] Huntress: PHP2Shell response report (supplied text)
- [3] PHP manual: session upload progress
- [4] PHP 8.4.4: session callbacks and ownership
- [5] PHP 8.4.4: hash-table operations
- [6] PHP 8.4.4: reference-counted destruction
- [7] PHP 8.5.4: session implementation
- [8] watchTowr: PHP RCE FAQ
- [9] PHP manual: session configuration
Need this checked in your own product?
Remediation & fix verificationWant to learn to find vulnerabilities like this yourself?
Secure code review course, one to one