It's the difference between leaving your front door unlocked and leaving a secret trap door under a rug that you first have to install yourself
CVE-2026-115560 is a PHP session upload progress vulnerability disclosed on 2026-10-23 as an informational advisory by the PHP project. It arises when an application (a) configures session.serialize_handler to php_binary or php_serialize in its .ini/.htaccess/.user.ini and then (b) calls ini_set('session.serialize_handler', 'php') at runtime before session_start(), while session.upload_progress.enabled remains On (the default). Under these conditions, the upload-progress callback writes session data using the config-level serializer, but session_start() reads it back with the runtime-switched php handler. The php handler treats | as a key-value delimiter, while php_serialize and php_binary do not — an attacker-controlled upload filename containing | followed by a serialized PHP object causes the php handler to split at the wrong boundary and call unserialize() on attacker-supplied data. The resulting object injection fires __destruct() / __wakeup(), enabling POP-chain-to-RCE. Affected: all PHP versions from 5.4.0 (when session.upload_progress was introduced) through current supported branches (8.2.x, 8.3.x, 8.4.x, 8.5.x). No patch is released yet; PHP will include the fix in the next regular bugfix release.
There is no vendor CVSS score for this CVE. PHP itself labels it an *unlikely RCE vector* and is handling it through the regular release cycle — not the emergency process used for the related php2shell CVEs (CVE-2026-109618/109619, CVSS 9.2). That framing is honest: the configuration pattern — deliberately switching serialize_handler at runtime before session_start() — is a code anti-pattern that no major framework (Laravel, Symfony, WordPress, Drupal, Magento) performs by default. PHP bug #71101 documented this handler-mismatch risk back in 2015 and was closed as a *documentation problem*. The practical population of vulnerable deployments is vanishingly small. However, the attack is pre-authentication (upload progress fires in SAPI before application code), so where the conditions *do* exist, exploitation is straightforward.
4 steps from start to impact.
Identify a handler-mismatch target
session.serialize_handler to php_binary or php_serialize in its configuration but then switches it to php via ini_set() before calling session_start(). This can be found through error-based disclosure, PHP info pages, or source-code access (open-source apps). No tooling specific to CVE-2026-115560 exists; reconnaissance would be manual or via custom Nuclei template against phpinfo() output.- Target PHP application performs runtime serialize_handler switch
- session.upload_progress.enabled = On (default)
- Application is reachable over HTTP/HTTPS
- Almost no production applications change serialize_handler at runtime
- No automated scanner fingerprints this configuration
- Attacker may need source-code review or phpinfo() leakage to confirm
ini_set.*session.serialize_handler is required.Craft a poisoned multipart upload
PHP_SESSION_UPLOAD_PROGRESS field is set in the POST body, and the uploaded file's filename contains a pipe | followed by a serialized PHP object payload (e.g., filename="garbage|O:8:\"Exploiter\":1:{…}"). Because session.upload_progress fires in the SAPI layer *before* application code, no application-level auth is needed. The session upload progress callback writes this data to the session file using the config-level serializer (php_serialize or php_binary), preserving the pipe and serialized payload verbatim.- A valid session cookie (PHPSESSID) — can be self-issued if session.use_strict_mode=Off (default)
- Target endpoint accepts multipart POST (most PHP endpoints do)
- session.use_strict_mode=On would require a pre-existing valid session ID
- Some WAFs strip or reject unusual filenames in multipart bodies
Trigger deserialization via handler mismatch
ini_set('session.serialize_handler', 'php') and then session_start(). The php handler reads the session file and splits it at the first | character. Everything after | is passed to unserialize(). The attacker's serialized object is instantiated, and PHP calls __wakeup() at creation time and __destruct() at request shutdown — even if the overall decode returns FAILURE.- Application must call ini_set() to switch handler to php before session_start()
- A usable POP chain must exist in loaded classes (framework, CMS, or application code)
- The runtime ini_set switch is the critical and extremely rare precondition
- POP chain availability depends on what libraries are loaded; vanilla PHP has limited gadgets
- PHP 8.x disabled many __wakeup() gadgets via ReadOnly properties
RCE via POP chain
__destruct() method chains through available class gadgets to achieve arbitrary code execution — typically via system(), exec(), file_put_contents(), or Monolog/Guzzle/SwiftMailer POP chains documented in PHPGGC. The attacker now has code execution as the PHP-FPM worker user (typically www-data or nobody). From here, standard post-exploitation applies: credential harvesting, lateral movement, web shell deployment.- POP gadget chain present in application dependencies
- No disable_functions restriction blocking the chain's terminal call
- Modern hardened PHP deployments use disable_functions to block system/exec
- Containerized PHP (Docker/K8s) limits lateral movement
- SELinux/AppArmor may block file writes outside the webroot
grep -rn "ini_set.*session.serialize_handler" /var/www/ across all PHP codebases. If no matches are found, the application is not vulnerable regardless of other settings. If matches exist, refactor to use a consistent handler configured at the ini level. This eliminates the root cause. Deploy within the 365-day noisgate remediation SLA.filename field contains PHP serialization markers (O:, a:, s: followed by length-prefixed strings). This blocks the payload delivery mechanism. Residual risk remains if an attacker finds an alternative injection point.- session.use_only_cookies=On — This was proven insufficient by CVE-2026-109618 research; the upload progress callback bypasses cookie-only restrictions because it writes to the session file directly in the SAPI layer, not through $_SESSION.
- disable_functions for system/exec — While this blocks many POP chain terminal calls, sophisticated chains can use
file_put_contentsto drop a webshell ormail()with-Xto write files, bypassing disable_functions entirely. - Upgrading to patched PHP versions (8.2.35 / 8.3.36 / 8.4.27 / 8.5.12) — These versions patch CVE-2026-109618/109619 but do NOT yet contain the fix for CVE-2026-115560. The fix will ship in the *next* regular bugfix release.
The supporting signals.
| In-the-Wild Exploitation | None observed. Not listed in CISA KEV. No reports from GreyNoise, Shadowserver, or vendor telemetry. All active exploitation telemetry (Amazon MadPot, Cloudflare, watchTowr) pertains to the sibling CVE-2026-109618/109619, not this variant. |
|---|---|
| Proof-of-Concept | No dedicated PoC for CVE-2026-115560. No repos found on GitHub or pocindex.io for this specific CVE ID. However, the underlying technique — serialize_handler mismatch via session.upload_progress — is thoroughly documented in PHPGGC and multiple CTF writeups since 2016. The php2shell PoC repos (e.g., mahumadad/php2shell-analysis) demonstrate the serializer-mismatch → POP chain vector that overlaps conceptually. |
| EPSS Score | Not yet scored. CVE-2026-115560 was disclosed on 2026-10-23; EPSS data typically lags 24–72 hours after NVD publication. Given the narrow preconditions, expect a low EPSS percentile (<5th). |
| KEV Status | Not listed. No CISA KEV entry as of 2026-10-23. |
| CVSS Vector | No vendor score published. Estimated CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H → 8.1 base (AC:H due to the rare runtime config requirement). The high AC pulls the realistic score down significantly. |
| Affected Versions | PHP 5.4.0 through current: 8.2.x (≤ 8.2.35), 8.3.x (≤ 8.3.36), 8.4.x (≤ 8.4.27), 8.5.x (≤ 8.5.12). All branches where session.upload_progress exists. PHP 8.1 and older are EOL and will not receive a fix. |
| Fixed Version | Not yet released. PHP will include the fix in the next regular bugfix release (expected within ~4 weeks of 2026-10-23). The session.upload_progress.enabled=0 workaround fully mitigates in the interim. |
| Scanning / Exposure | Shodan and Censys show ~5.8M internet-facing PHP hosts as of October 2026, but the fraction performing runtime serialize_handler switching is estimated at <0.1% — no mass-exposure signal exists. GreyNoise shows no scanning activity targeting this specific variant. |
| Disclosure Date | 2026-10-23 — published as an informational advisory on the php.webmaster mailing list, two days after the php2shell emergency patches. |
| Credit / Origin | Discovered during the php2shell investigation. The PHP security team identified it as a leftover variant of the session upload progress attack surface. No individual researcher credited. PHP bug #71101 (filed 2015) first documented the handler-mismatch risk. |
Sources.
- PHP Informational Advisory (php.webmaster mailing list)
- GHSA-6wmf-4rv5-p3rp — PHP RCE via stale global state (sibling CVE-2026-109619)
- PHP Bug #71101 — serialize_handler must not be switched for existing sessions (2015)
- Sansec — PHP2Shell: PHP RCE Research
- Wiz — PHP2Shell: AI uncovers critical RCE vulnerabilities
- CERT-EU Security Advisory 2026-018 — Critical Vulnerabilities in PHP
- UW IT — Critical PHP vulnerabilities (Oct 2026)
Why this verdict
- Pre-auth RCE is real but gated by an extreme precondition. The attack fires before application code runs (in the SAPI rfc1867 callback), so no authentication is needed — but *only* if the application performs a runtime
ini_set('session.serialize_handler', 'php')switch. This is a deliberate code pattern that no major PHP framework (Laravel, Symfony, WordPress, Drupal, Magento, Joomla) uses by default. PHP's own security team calls it "unlikely in practice." - Attack Complexity is HIGH under CVSS 3.1 semantics. The attacker cannot create the vulnerable condition — it must pre-exist in the target application's code. There is no way to force the runtime handler switch remotely. This is a true AC:H scenario: the attacker needs a specific, rare application-level behavior they cannot influence.
- No PoC, no exploitation, no scanning. As of 2026-10-23, there are zero public PoCs targeting this specific CVE, zero KEV entries, zero GreyNoise tags, and zero incident reports. The broader php2shell exploitation wave targets CVE-2026-109618/109619, not this variant.
- Mitigation overlap with php2shell. Any organization that applied the recommended
session.upload_progress.enabled=Offmitigation for the CVSS 9.2 php2shell CVEs (two days earlier) is *already fully protected* against CVE-2026-115560. The incremental risk from this CVE for teams that responded to php2shell is zero. - Role multiplier: PHP powers ~77% of web servers, spanning every deployment role from dev sandboxes to production e-commerce and healthcare portals. In high-value roles (Magento storefronts, SaaS backends, healthcare FHIR endpoints), a successful chain yields RCE as
www-data— host-level compromise with potential for lateral movement. However, the chain only succeeds if the application code performs the runtime handler switch, which is not correlated with deployment role. No known high-value application performs this pattern. The estimated fraction of PHP installs where this chain succeeds is <0.1%, well below the 1% floor threshold. The blast radius *per successful exploitation* is HIGH (host-level RCE), but the reachable population is negligible, keeping the aggregate severity at MEDIUM.
Why not higher?
Upgrading to HIGH would require either (a) evidence that ≥1% of PHP installations perform the runtime serialize_handler switch, or (b) a specific widely-deployed application/framework that does this by default, or (c) active exploitation / public PoC lowering the attacker's cost. None of these conditions are met. The PHP team's own decision to defer this to the regular release cycle — rather than the emergency process used for CVE-2026-109618/109619 — is strong signal that the vendor's own threat model considers the practical risk low.
Why not lower?
Downgrading to LOW would understate the impact when the preconditions *are* met: this is unauthenticated, pre-auth RCE with no user interaction required. The underlying deserialization technique is well-understood, PHPGGC provides ready-made POP chains for dozens of frameworks, and the fix is not yet available. Any custom PHP application that *does* switch handlers at runtime is fully exposed with no patch to deploy. The attack surface is narrow but the impact is maximum severity.
Crowdsourced verification payload.
Run this script on each PHP application host as root (or any user with read access to php.ini and the webroot). Invoke with: sudo bash check_cve_2026_115560.sh /var/www/html where the argument is the path to your PHP application code. The script checks (1) whether session.upload_progress is enabled, (2) whether any PHP files perform a runtime serialize_handler switch, and outputs VULNERABLE / PATCHED / UNKNOWN.
#!/usr/bin/env bash
# check_cve_2026_115560.sh — CVE-2026-115560 exposure checker
# Usage: bash check_cve_2026_115560.sh [/path/to/webroot]
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
WEBROOT="${1:-/var/www/html}"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
# Step 1: Check if PHP is installed
if ! command -v php &>/dev/null; then
echo -e "${YELLOW}[UNKNOWN]${NC} PHP is not installed on this host."
exit 2
fi
PHP_VERSION=$(php -r 'echo PHP_VERSION;' 2>/dev/null || echo "unknown")
echo "[*] PHP version: $PHP_VERSION"
# Step 2: Check session.upload_progress.enabled
UPLOAD_PROGRESS=$(php -r 'echo ini_get("session.upload_progress.enabled") ? "1" : "0";' 2>/dev/null || echo "unknown")
if [ "$UPLOAD_PROGRESS" = "0" ]; then
echo -e "${GREEN}[PATCHED]${NC} session.upload_progress.enabled is Off — CVE-2026-115560 is fully mitigated."
exit 0
fi
if [ "$UPLOAD_PROGRESS" = "unknown" ]; then
echo -e "${YELLOW}[UNKNOWN]${NC} Could not determine session.upload_progress.enabled setting."
exit 2
fi
echo "[!] session.upload_progress.enabled is On (default)."
# Step 3: Scan webroot for runtime serialize_handler switching
if [ ! -d "$WEBROOT" ]; then
echo -e "${YELLOW}[UNKNOWN]${NC} Webroot '$WEBROOT' does not exist. Cannot scan for handler switching."
echo " session.upload_progress.enabled is On — apply mitigation if handler switching exists in your code."
exit 2
fi
echo "[*] Scanning $WEBROOT for ini_set('session.serialize_handler',...) calls..."
MATCHES=$(grep -rlnE "ini_set\s*\(\s*['\"]session\.serialize_handler['\"]" "$WEBROOT" --include='*.php' 2>/dev/null || true)
if [ -z "$MATCHES" ]; then
echo -e "${GREEN}[PATCHED]${NC} No runtime serialize_handler switching found in $WEBROOT."
echo " session.upload_progress.enabled is On, but the app does not switch handlers."
echo " CVE-2026-115560 preconditions are NOT met."
exit 0
fi
echo -e "${RED}[VULNERABLE]${NC} Runtime serialize_handler switching detected AND session.upload_progress.enabled is On."
echo " Matching files:"
echo "$MATCHES" | while read -r f; do
LINE=$(grep -nE "ini_set\s*\(\s*['\"]session\.serialize_handler['\"]" "$f" | head -3)
echo " $f: $LINE"
done
echo ""
echo " REMEDIATION: Set session.upload_progress.enabled=Off in php.ini and restart PHP-FPM,"
echo " or refactor the application to use a consistent serialize_handler."
exit 1