Netenrich Blog | Expert Cybersecurity Insights on SecOps, threats & more

php2shell (CVE-2026-109618 & CVE-2026-109619): Fix & Mitigation

Written by Netenrich | Thu, Oct 22, 2026 @ 10:04 AM

Most critical vulnerabilities need something to line up before they become dangerous: an exposed admin panel, a plugin nobody patched, a misconfiguration someone made years ago. php2shell needs none of that. It sits in a feature PHP turns on by default, and it needs no login, no session cookie, and no upload form.

On October 20, 2026, details of two critical flaws in the PHP core, CVE-2026-109618 and CVE-2026-109619, leaked publicly before coordinated disclosure. Researchers at watchTowr have since independently reproduced CVE-2026-109618. Defenders are now working against a clock that started before the patches were announced.

Here is what you need to know:

  • What it is: a heap use-after-free in PHP’s session upload progress handler, leading to pre-authentication remote code execution. CVSS v4.0: 9.2 (Critical).
  • Who is exposed: virtually every PHP deployment, because the vulnerable feature (session.upload_progress.enabled) is on by default. That includes CMS platforms (WordPress, Drupal, Joomla, Magento), hosting control panels (cPanel, Plesk), internal appliances, and bundled PHP binaries.
  • What an attacker gets: a single crafted multipart HTTP request runs arbitrary operating system commands as the web server user. From there, webshells, data theft, and lateral movement follow.
  • What to do: upgrade to PHP 8.5.12, 8.4.27, 8.3.36, or 8.2.35. If you can’t patch yet, a one-line configuration change neutralizes the attack vector without breaking normal file uploads. End-of-life branches (8.1 and older) will not get official patches.
  • Edge protection: Cloudflare has deployed managed WAF rules on all plans. Custom rules for other WAFs are below.


How php2shell works


Root cause

The flaw lives in php_session_rfc1867_callback, PHP’s RFC 1867 upload progress handler in ext/session/session.c. To track upload progress in the session, it caches two raw zval * pointers, post_bytes_processed and current_file_bytes_processed. Both point directly into buckets of a live HashTable. When that HashTable is rebuilt or replaced during a progress update, the cached pointers are left dangling.


Exploitation flow

  1. Trigger: a multipart POST request that includes the upload progress field (PHP_SESSION_UPLOAD_PROGRESS by default) activates progress tracking.
  2. Free: a progress update cycle re-reads session data. zend_hash_update() destroys the previous data tree via zval_ptr_dtor, freeing memory the cached pointers still reference.
  3. Reclaim: attacker-controlled data reclaims the freed memory, so the dangling pointer now references attacker-shaped structures.
  4. Execute: PHP invokes zend_call_function() through the corrupted structures, resulting in arbitrary command execution (for example, via system()).


Why it’s easy to reach

  • Any PHP endpoint works. No authentication, application logic, or upload functionality is required. If PHP serves it, it’s reachable.
  • No session cookie needed. A session can be started purely from the multipart field; the PHPSESSID cookie plays no part in the exploit.
  • More than POST. On PHP 8.4, request_parse_body() parses multipart bodies on PUT and PATCH as well as POST.
  • A header variant that slips past naive rules. PHP treats multipart/form-data,boundary=x (a comma instead of a semicolon) as valid multipart. Detection rules must account for both this and the extra HTTP methods.
  • Confirmed on real builds. The flaw is confirmed on standard php-src builds, including 8.4.4 and 8.2.34. Public research adapts the exploit to distribution binaries (Debian Bookworm, Ubuntu Noble and Jammy) using symbol offsets.

The upstream fix

The fix replaces the cached zval * pointers with plain zend_long counters. It also introduces php_session_rfc1867_sync_counters(), which re-resolves array entries on every update instead of holding HashTable bucket pointers. A community source patch implementing the same approach is available for organizations that build PHP from source.


Who is exposed

Every PHP branch from 5.4 onward is affected. Supported branches have emergency releases; end-of-life branches do not, and each needs its own plan.

PHP branch Support status Affected Fixed version What to do
PHP 8.5 Active < 8.5.12 8.5.12 Upgrade (emergency release)
PHP 8.4 Active < 8.4.27 8.4.27 Upgrade (emergency release)
PHP 8.3 Active < 8.3.36 8.3.36 Upgrade (emergency release)
PHP 8.2 Security only < 8.2.35 8.2.35 Upgrade (emergency release)
PHP 8.1 End of life All 8.1.x No official patch Mitigate, use Extended Lifecycle Support (ELS), or apply a distro backport
PHP 8.0 End of life All 8.0.x No official patch Configuration mitigation is mandatory
PHP 7.x End of life 7.0 to 7.4 No official patch Upgrade or mitigate urgently
PHP 5.x End of life 5.4.0+ No official patch Isolate the host or disable the feature


For PHP 8.1 and older, your options are vendor or distribution backports, ELS, the community source patch, or the configuration mitigation below. Restrict exposure until you can upgrade, and plan migration of every end-of-life installation to a supported branch.


What to do now


1. Patch and verify

Upgrade to PHP 8.5.12, 8.4.27, 8.3.36, or 8.2.35, or the equivalent patched package from your OS distribution. Then restart PHP-FPM and the web server so running workers load the fixed binary:

Listing 2: PHP-FPM and Apache Service Restart Commands

# Restart the appropriate service based on your server configuration

sudo systemctl restart php8.3-fpm  # Debian/Ubuntu units are versioned; use your branch
sudo systemctl restart php-fpm      # RHEL-family and most source builds
sudo systemctl restart apache2      # or httpd, if PHP runs as an Apache module


Verify the version the web server is actually running on every host. php -v and php -i both report the CLI build, which can differ from what serves traffic. Check the FPM binary directly (php-fpm8.3 -v) or load a phpinfo() page through the web server, then remove it.


2. Mitigate where you can’t patch yet

Disabling session upload progress neutralizes the attack vector. Normal file uploads keep working; only the session-based upload progress meter is turned off.

In php.ini:

session.upload_progress.enabled = Off

Or per PHP-FPM pool (for example, /etc/php/*/fpm/pool.d/www.conf):

php_admin_flag[session.upload_progress.enabled] = off

FrankenPHP (global Caddyfile block):
  {
frankenphp {
php_ini session.upload_progress.enabled 0
  }
}

Reload services afterward (systemctl reload php-fpm or apache2). Then confirm through a web-served phpinfo() page that the setting is Off for the web SAPI, not only the CLI.


3. Add WAF rules as defense in depth

WAF rules are a layer on top of patching, not a replacement for it. WAFs parse multipart bodies differently from PHP, so evasion is possible. If you’re on Cloudflare, managed rules are already deployed on all plans. The custom rules below use Cloudflare expression syntax; adapt them for ModSecurity or your WAF.

Rule A (surgical; requires body inspection, e.g., Cloudflare Enterprise). Blocks only requests carrying the progress field, so legitimate uploads keep working:

any(http.request.body.form.names[*] == "PHP_SESSION_UPLOAD_PROGRESS")

Rule B (broad; works on all plans). Blocks multipart requests on POST, PUT, and PATCH except on allowlisted upload routes. Adapt the path list to your application:

(any(lower(http.request.headers["content-type"][*]) contains
"multipart/form-data"))
and (http.request.method in {"POST" "PUT" "PATCH"})
and not (http.request.uri.path in {"/upload" "/api/upload" "/wp-admin/async-upload.php"})


Two details matter here:

  • Rule B uses contains rather than starts-with, so it also catches the comma variant multipart/form-data,boundary=x.
  • If session.upload_progress.name has been customized in php.ini, substitute that name in Rule A.

Do not block on the PHPSESSID cookie or query parameter. It’s PHP’s default session cookie, so blocking it takes the site down for logged-in users. It also isn’t what the exploit uses, and apps that call session_name() bypass it trivially.


Detection and threat hunting

With details public since October 20, treat any unpatched, internet-facing PHP host as a hunting priority. Successful exploitation typically shows up as anomalous child processes spawned by PHP worker processes: php-fpm, php-cgi, php-cgi.exe, httpd, or apache2. Alert when one of these parents spawns any of the following:

Category Child processes / patterns
Interactive shells /bin/sh, /bin/bash, cmd.exe, powershell.exe
Script interpreters python, perl
Network / transfer tools curl, wget, nc, certutil.exe
Reverse-shell patterns /dev/tcp/, fsockopen, mkfifo
Reconnaissance commands whoami, id, uname -a, ipconfig


Where to look:

  • EDR/SIEM: alert on any process creation where the parent is a PHP worker or web server and the child matches the table above.
  • Request logs: review multipart POST, PUT, and PATCH requests containing PHP_SESSION_UPLOAD_PROGRESS since October 20, 2026, especially to endpoints that never accept uploads. Standard access logs don’t record request bodies, so the field name won’t appear there. Use WAF logs or body-capture logging, and in access logs look for multipart requests to non-upload paths.
  • File integrity: inspect web roots, /tmp, and /var/tmp for newly created PHP files, webshells, or transient execution artifacts.


The real test: knowing, not guessing

When a flaw lands in a component as foundational as PHP, the patch is the easy part. The hard part is answering three questions quickly and with evidence.

  • Where are we exposed? PHP hides in places asset inventories miss: appliances, bundled binaries, forgotten marketing sites, and containers built from old base images. Exposure has to be confirmed host by host, down to the version the web server actually runs.
  • Which exposure matters first? Internet-facing production systems outrank isolated internal services. Effort should go where exposure and exploitability intersect.
  • Are we already compromised? An unusual HTTP multipart request and a shell spawned by php-fpm look on the webserver like unrelated alerts across two different tools. Together, they are part of one incident.

That last question is where most teams lose time. Netenrich Autonomous Security Operations (ASO) correlates WAF, web server, and endpoint signals into a single, actionable incident, so analysts investigate one story instead of triaging thousands of disconnected alerts. It applies the same approach to emerging threats like php2shell: hunting across your environment and confirming whether controls are holding.

Patching closes the door. Proving it stayed closed is what gives security leaders confidence.

 


References

 
About the Author 


 

Ganesh Sunkari

Ganesh Sunkari serves as Director of Security Engineering at Netenrich, where he drives security content innovation for next-generation Agentic SOC environments. He leads the development of autonomous workflows that streamline how high-fidelity detections are engineered, validated, and operationalized against emerging threats.

Bringing extensive expertise across MITRE ATT&CK-aligned adversary analysis, threat hunting, and security investigations, Ganesh is passionate about leveraging intelligent automation to transform reactive SOC models into proactive, intelligence-driven defense engines.