DEV Community

Cover image for The 'BREAKING' unpatched PHP RCE that wasn't: what CVE-2026-115560 actually requires
Sam LABBE
Sam LABBE

Posted on

The 'BREAKING' unpatched PHP RCE that wasn't: what CVE-2026-115560 actually requires

The alert going around today: ‼️ BREAKING ‼️ PHP warns of another UNPATCHED RCE vulnerability just 2 days after releasing emergency patches. New "CVE-2026-115560" affects only unusual session configurations. PHP has issued urgent mitigation guidance. Three lines, each one a little wrong. The actual advisory — php.net, today, 300 words, measured — says: it's an informational advisory, handled in the regular release cycle, reachable only through a config combo PHP itself calls "unlikely in practice", and the mitigation is the same one from Tuesday — if you applied it then, "keep it in place and no further action is needed."

This is the third amplification story of the week on this blog, and the easiest to settle — because unlike LiteLLM's scrambled dates or eMudhra's "out of proportion" complaint, this one comes with a checklist you can run in three commands. By the end you'll know whether CVE-2026-115560 concerns you, and more usefully, how to read the next BREAKING without the exclamation marks.

⚠️ The full lab lives under: the two-request PoC that confirms the injection locally (no Docker — the PHP CLI's built-in server is the whole stack, and the serializer grammar is not version-specific), plus an audit script that turns the decision tree below into one command with an exit code. Local target only, never aimed at third-party systems.

GitHub logo slabbdev / php-cve-2026-115560-kit

Local-only PoC lab for CVE-2026-115560 (PHP session upload-progress serialize-handler confusion) — two-request PoC on the PHP CLI built-in server (no Docker) + fleet audit implementing the advisory's decision tree.

cve-115560-kit — local lab for the PHP session serialize-handler confusion

Companion lab for the black box article on CVE-2026-115560 (PHP session.upload_progress informational advisory, 2026-10-23). Part 2 of the php2shell story (part 1: CVE-2026-109618/109619).

Local target only (127.0.0.1). Never aimed at third-party systems.

The combo (all three legs required, per php.net)

  1. session.serialize_handler = php_binary or php_serialize in configuration;
  2. application code switches it back to php at runtime with ini_set() before session_start();
  3. session.upload_progress.enabled still on.

PHP calls this setup "unlikely in practice" — the advisory is informational, the fix ships in the regular cycle. The mitigation is the same as GHSA-6wmf-4rv5-p3rp (CVE-2026-109619): session.upload_progress.enabled=0.

The lab

No Docker needed — the PHP CLI's built-in server is the whole stack, and the serializer grammar is not version-specific. Requires php and python3 on $PATH.

$ ./run.sh          # starts 127.0.0.1:8899 (or ./run.sh PORT) and runs the PoC
Enter fullscreen mode Exit fullscreen mode

What the two…

What the advisory actually says

php.net, October 23, verbatim:

We suggest you set session.upload_progress.enabled=0 until the next regular bugfix release, to fully protect from an unlikely RCE vector. This setting was already listed as a mitigation in the GHSA-6wmf-4rv5-p3rp advisory for CVE-2026-109619 published on Oct 21st, 2026. If you applied it then, keep it in place and no further action is needed.

The issue can only occur under an unusual combination of configuration and application code. An application must set session.serialize_handler to php_binary or php_serialize in its configuration and then switch it to php at runtime with ini_set() before calling session_start(). Session upload progress must also still be enabled. Applications that never change the serialize handler at runtime can keep the feature enabled. Since this setup is unlikely in practice, we are handling it in the regular release cycle and will publish a full advisory for CVE-2026-115560 with the next release.

Count what changed between that text and the BREAKING version: "informational advisory" became "unpatched RCE"; "regular release cycle" became "urgent"; "unlikely in practice" became a vague "unusual session configurations" that reads like it could be you. None of the repost's three lines survives contact with the primary source.

The bug, in one paragraph

(Interpretation flagged as such — the full advisory ships with the next release.) The session store is written by one serializer and, after the runtime switch, parsed by another. php_binary and php_serialize have different key-encoding rules than the default php handler — so bytes written under one grammar get read under another, and the upload-progress entry conveniently contains an attacker-chosen key name. Serializer confusion on attacker-influenced session content is the classic path to code execution; the exact primitive waits for the full advisory. What PHP is telling you today is precisely who needs to care, which is the rarest and most useful sentence in vulnerability disclosure.

The lab: watching the confusion happen

The kit runs on the PHP CLI's built-in server — no Docker, because the combo is pure configuration and the serializer grammar is not version-specific:

git clone https://github.com/slabbdev/php-cve-2026-115560-kit
cd php-cve-2026-115560-kit && ./run.sh   # binds 127.0.0.1:8899 only
Enter fullscreen mode Exit fullscreen mode

run.sh starts the target with session.serialize_handler=php_binary in configuration (leg 1) and upload progress enabled (leg 3); the read path in target/index.php performs the ini_set() switch (leg 2). Then the PoC fires two requests:

  1. a small multipart upload whose PHP_SESSION_UPLOAD_PROGRESS value is the key p|O:8:"DemoVuln":1:{...} — the progress hook stores the entry under the configured grammar, where a | inside a key means nothing;
  2. a GET that hits the ini_set() path: the same session file is now parsed under the php grammar, which splits entries at the first | — the one the attacker planted — and unserializes whatever follows.

Real output, PHP 8.5.10:

[1] upload POST — writes the progress entry under the crafted key…
     HTTP/1.1 200 OK
[2] read GET — ini_set to `php`, session_start()…
     HTTP/1.1 200 OK
    > [CVE-2026-115560] DemoVuln::__wakeup — attacker-controlled unserialize executed, note=pwned-1
    > read OK — session keys: []
RESULT: INJECTION CONFIRMED
Enter fullscreen mode Exit fullscreen mode

Two details in that output carry the lesson:

  • session keys: [] — the decoder chokes on the progress-array bytes that trail the injected object, and destroys the session. The gadget already ran. From the attacker's side the corruption is free; from yours, the session silently resets.
  • the server logs PHP Warning: session_start(): Failed to decode session object. That warning is your detection artifact — a store written under one grammar and read under another shows up exactly there. If you must keep the runtime switch, alert on this line.

And no race is involved: session.upload_progress.cleanup=0 parks the poisoned entry in the store after the upload completes, so request 2 can come hours later. With Tuesday's bug this CVE shares the feature; what it doesn't share is any need for timing skill.

The decision tree

  • You set session.upload_progress.enabled=0 on Tuesday (the php2shell mitigation, GHSA-6wmf-4rv5-p3rp): you're already protected from this vector too. Nothing to do.
  • You kept upload progress on and your application never calls ini_set() on session.serialize_handler: keep the feature enabled — the combo doesn't exist on your fleet. Apply Tuesday's patches if you haven't (8.5.12, 8.4.27, 8.3.36, 8.2.35).
  • You do switch serialize handlers at runtime (some frameworks and session shims have done this historically): set upload progress to 0 now, and watch for the full advisory with the next release.

The lab: three lines to know

# 1. Who configures the serialize handler at all?
grep -rn "session.serialize_handler" /etc/php* src/ config/ 2>/dev/null

# 2. Does any code switch it at RUNTIME? (this is the rare leg of the combo)
grep -rn "ini_set.*serialize_handler" src/ vendor/ --include='*.php' 2>/dev/null | grep -v tests

# 3. What is the effective config on this box?
php -i | grep -E 'session.serialize_handler|upload_progress.enabled'
Enter fullscreen mode Exit fullscreen mode

Reading the output:

  • Command 2 returns nothing → you're in "applications that never change the serialize handler", full stop. The advisory says you can keep upload progress on.
  • Command 2 returns a hit → you're the fleet the advisory was written for: session.upload_progress.enabled=0 in your php.ini now, until the patched release.
  • Command 3 shows upload_progress.enabled => On and you already turned it off — check you edited the right ini file (FPM pools and CLI routinely read different ones; php -i on the CLI proves nothing about php-fpm's view, repeat it through a phpinfo() page or php-fpm -i).

That last one is the trap I'd bet on: Tuesday's mitigation applied to the CLI config, the FPM pool still streaming uploads with progress enabled. The advisory is three hundred words; the operational reality is one ini file per SAPI.

The kit in the lab above packages the audit as runnable code too: ./audit-cve-2026-115560.sh -s src/ -c config/ runs the three checks, reads the effective config per SAPI instead of trusting php -i, compares your branch against the patched releases (8.5.12, 8.4.27, 8.3.36, 8.2.35), and exits 0 / 1 / 2 so CI can gate on it (--json if you'd rather parse than read).

Why this post exists

Look at the week: LiteLLM's supply-chain compromise got reposted with dates scrambled into the wrong month; ZDI's private queue leaked onto the public LKML; a CA chairman blamed "social media amplification" for scrutiny he'd priced wrong; and today, a measured informational advisory got dressed as a BREAKING RCE. Four different failure modes, one common denominator: the repost is cheaper than the read, and every step away from the primary source loses accuracy and gains urgency.

The skill this blog keeps coming back to is boring and decisive: open the advisory. php.net's text is the counterexample to its own amplification — no exclamation marks, an honest likelihood estimate ("unlikely in practice"), a precise trigger condition, and the sentence almost no disclosure ever contains: if you already did the thing, do nothing. That last sentence is why people should read sources: most days, the correct action is no action, and only the primary source will tell you so.

Sources: php.net — Informational advisory: Session upload progress (Oct 23, 2026) · GHSA-6wmf-4rv5-p3rp / CVE-2026-109619 advisory · Part 1 — the php2shell lab

TL;DR — CVE-2026-115560 is real but requires a serializer switched at runtime plus upload progress enabled; PHP calls it unlikely in practice and ships the fix next release. Tuesday's mitigation already covers you. Three grep commands — or the kit's audit script — tell you which case you're in. The BREAKING version of this story is the case study; the advisory is the answer.

Top comments (3)

Collapse
 
kielltampubolon profile image
Kiell Tampubolon •

Thanks for going back to the advisory text. "Informational, regular release cycle, unlikely in practice" turning into "BREAKING unpatched RCE" is a pattern worth calling out every time, because alert fatigue is how the real ones get missed.

The habit I'd pull out of this for readers: read the preconditions before the score. Here it's three at once, a non-default serialize handler, a runtime switch with ini_set() before session_start(), and upload progress still on. Each one shrinks the exposed population a lot.

One small tip for the grep step: run it over vendor/ too, not just your own code. A runtime ini_set() for the session serializer is more likely to come from an old session library or framework shim than from code anyone on the team remembers writing.

Collapse
 
slabb profile image
Sam LABBE •

The vendor/ addition is the right one — a runtime ini_set() for the session serializer is exactly the kind of thing a framework shim sets six layers below anyone's memory. Preconditions before score, and grep over the whole tree: that pair turns a "BREAKING" alert into a five-minute disposition.

Collapse
 
kielltampubolon profile image
Kiell Tampubolon •

"Preconditions before score" is a good way to put it. Most of the panic around that alert came from reading the score first and the requirements never. Thanks Sam, this was a good thread.