All research

CVE-2026-109618: we tried to make the PHP RCE fire. It wouldn't.

On 21 October, PHP shipped an emergency patch for a critical remote-code-execution bug in its own interpreter, and a security vendor predicted mass exploitation within about fifteen minutes, because diffing an official fix against the vulnerable source is the easiest exploit lead an attacker ever gets. We spent that same window doing the unglamorous version of the work: building the bug from source, instrumenting it, and trying to make it actually execute code. On everything we tested, it wouldn't. The distance between "this is vulnerable" and "this is owned" turned out to be the whole story, and it is a distance a version-number scanner cannot see.

The blunt version CVE-2026-109618 and CVE-2026-109619 are real, critical PHP core RCE bugs (CVSS 9.2) in the session.upload_progress feature, patched 2026-10-21, backported even to EOL 7.4/8.0/8.1. The trigger runs before any application code, so an app-layer WAF cannot stop it. The leaked PoCs' stated root cause, a use-after-free inside one request, does not hold on modern PHP; a refcount guard keeps the data alive. The real bug is a harder cross-request use-after-free that needs a php-fpm worker to survive an out-of-memory abort. On stock Docker images (8.4.4 and EOL 7.4.33 alike) the worker always exits instead, and the window never opens. As of this writing nobody has published a working, generic RCE against php-fpm. PHP's own advisory says a working exploit exists, but only for mod_php, not php-fpm. Patch everything anyway.

What the bug actually is

session.upload_progress is a convenience feature: it lets a browser poll how far along a large file upload is, by having PHP write the upload's progress into the user's session while the multipart request body is still streaming in. You switch it on for a request by including a form field named PHP_SESSION_UPLOAD_PROGRESS in a multipart POST. The important part for an attacker is where that code lives: in PHP's native request parser (main/rfc1867.c and ext/session/session.c), which runs before a single line of application code executes. That is why this earns its CVSS 9.2 and why no amount of application-layer input validation or WAF rules helps: the dangerous work happens while PHP is still reading the body, before your app, your framework, or your filters ever see the request.

Because the flaw is in PHP core, not in any one framework, it reaches everything that runs on PHP: Magento, Shopware, WooCommerce, WordPress, Laravel, Symfony. The exposure is wide. Shodan-based figures cited in press coverage put roughly 884,000 internet-facing hosts on potentially affected PHP versions, and the EOL branches (8.1 and below) are the worst off, because most distributions will never ship a packaged fix, only a source backport. That is exactly the small-and-medium-business software stack we see day to day.

The leaked PoCs, and what they actually prove

The CVE was leaked ahead of PHP's own coordinated disclosure, and a cluster of PoC repos went up on GitHub (the most-discussed being Zer061 and 0x4148, with others from junaire, WoYinDao, wakemeiflose, ikow and eness). They all describe the same root cause: a stale, dangling zval* pointer that gets freed between two session.upload_progress updates inside a single request: a textbook use-after-free.

We reviewed those repos directly, line by line, before touching any code. A few things were worth noting. The Zer061 account is not a throwaway: created in 2017, 42 repos, real follower graph. Its writeups (VALIDATION.md, REPORT_VALIDATION.md) are source-line-verified against real php-src 8.4.4 and 8.2.34, and line up with independently published research. We scanned the included PoC script for the usual nasties (exfiltration, hardcoded command-and-control, obfuscation) and it came back clean: raw sockets to a user-supplied host, not a trojan.

One detail did stand out. The repo's REPORT_VALIDATION.md carries an appendix framed as an "ORIGIN-ACCESS RCE RUNBOOK", with <REDACTED> target references and a specific redacted endpoint path. That reads less like an academic writeup built from scratch and more like a sanitized operational log from an engagement of unknown authorization. We did not execute any of that PoC code, even in a sandbox. It wasn't necessary for a defensive reproduction, and we had no interest in operationalizing something of uncertain origin.

So we built it ourselves, and ASAN stayed silent

Everything from here is our own lab work, run in disposable Docker sandboxes with --network none. No live target, no client system, no third party touched: just official PHP images and source builds in a box with no way out.

We compiled PHP 8.4.4 from source with AddressSanitizer: full memory-error instrumentation, system allocator, a 256 MB quarantine so freed memory is not reused quickly enough to hide a use-after-free. Then we ran the public repos' described trigger (the "stale zval* freed across session.upload_progress updates" mechanism) across 12 live attempts, six variants each of what the repos call bug618 and bug619, with a harmless marker payload (echo PWNED_SANDBOX_TEST, never anything destructive).

Every single run reached the trigger code path. Both "armed" pointers got written through, every time. And ASAN reported nothing: zero memory-safety errors, across every variant. So we stopped guessing and read the source. In session.c around lines 3182–3190 there is a Z_TRY_ADDREF refcount bump that keeps the relevant data alive at exactly the moment the public repos claim it gets freed. Conclusion: the publicly described root cause does not hold on modern PHP. The repos prove their trigger reaches the vulnerable area. They do not prove a use-after-free actually fires there.

The real mechanism is harder, and it crosses requests

A silent ASAN is not an all-clear: it means we were looking in the wrong place. Cross-referencing independent researchers and vendors (Sansec, RemiPelloux, Phoenix Security, watchTowr) pointed at a different, nastier mechanism. The real bug is an out-of-memory-triggered cross-request use-after-free:

That is the actual use-after-free: not within one request, but across two requests that happen to share a worker process that lived through the abort. It is a genuinely sound bug. It is also a much more demanding one to land than "send two upload-progress updates," because it hinges entirely on that one word: survives.

We got the OOM every time. The worker died every time.

On the official php:8.4.4-fpm-bookworm image, the out-of-memory event fired exactly as the theory predicts: the fatal "allowed memory size exhausted" error, at the right allocation size, on the right code path. The primitive works. But the worker process always exited after the bailout, with exit code 70, and the master immediately spawned a fresh one. The surviving-worker condition the attack depends on never occurred, so the cross-request reuse window never opened.

That matches the external picture: RemiPelloux's research suggests worker survival sits in a narrow, build-and-memory_limit-specific band, roughly 68–88 MB at a 256 MB limit. In other words, it may well be reachable on some real-world configurations; it just is not the behavior of a stock image. That is the crux of the whole CVE: "vulnerable" and "exploitable" are separated here by a thin, config-dependent margin.

A false positive on EOL 7.4, and how we caught it

PHP 7.4 is end-of-life and gets no packaged distro fix, so it is where the real-world risk concentrates. We pushed harder there, on the official php:7.4.33-fpm image, across three rounds.

Round 1 looked like a breakthrough. The worker appeared to survive the OOM bailout in all 18 test configurations: memory_limit from 16M to 128M, across three php-fpm process-manager modes. The exact opposite of 8.4.4. For a moment it looked like EOL PHP was the soft target.

It was a false positive, and we caught it ourselves. The test harness was running with php-fpm's default post_max_size=8M, which silently rejects oversized multipart bodies before upload-progress parsing ever runs. So the OOM never actually fired: the worker "survived" trivially because the crash path was never reached. We were measuring our own test rig, not the bug. (A good reminder that the thing most likely to fool you in this work is your own instrumentation, not the target.)

Rounds 2 and 3, corrected (post_max_size=0, OOM genuinely firing on every request): first, the same Z_TRY_ADDREF refcount guard is present in 7.4.33 too (session.c line 3151), so the original public-repo mechanism does not hold on 7.4 either. For the real OOM mechanism, we have direct log evidence rather than inference: php-fpm's own stderr shows every worker child exiting with code 70, one to two seconds after an OOM-triggering request, every time, in every config, and the master respawning a clean worker each time. The cross-request reuse never becomes possible. 7.4.33 behaves identically to 8.4.4 for this bug. We did not achieve RCE on either.

Has anyone actually done it? Not publicly, except on mod_php

In parallel we went looking for anyone who had achieved and evidenced real RCE: not a crash, not "the code path is reached," not a probe tool that only detects the vuln, and not a chain that needs a second unrelated bug on top. As of 2026-10-21, we could not find one. Every public repo we examined falls into one of three buckets: an unreproducible UAF claim (the original mechanism, which we disproved), an explicit "this is a vulnerability probe, not an exploit" tool, or a chain that additionally requires an app-side bug (a local file inclusion, or a deserialization gadget class already present in the target app) stacked on top of the CVE. That last kind is not a generic, CVE-alone RCE.

Several vendors (Hadrian, Cloudflare's CTO in press quotes, The Stack) describe attacks "emerging" or happening "at scale," but without evidence: no IOCs, no logs, no samples. That language describes exploitation attempts, which absolutely are happening, not confirmed successful code execution. Huntress stated plainly they have neither built a working PoC nor seen in-the-wild exploitation. One account (ikow) claims a full weaponized exploit behind a subscriber paywall: unverified, could be real, could be vapor.

The one genuinely new and significant fact came from PHP's own advisory: it states there is "a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs," but scopes it specifically to mod_php, the older Apache-embedded PHP module, not php-fpm. PHP did not publish the exploit or name who built it. And this lines up precisely with our findings: mod_php's process model keeps the Apache child process alive across requests by design, so the "does the worker survive the bailout" question that blocked us on php-fpm simply does not arise. On mod_php, the cross-request reuse window comes for free.

What this means for you

Separate what we verified ourselves from what we are citing. We built the bug under ASAN, confirmed the OOM primitive fires on both 8.4.4 and EOL 7.4.33, and showed with direct exit-code and refcount-source evidence that stock php-fpm workers die on the bailout and the public single-request UAF does not hold. The mod_php exploit, the 884k-host Shodan figure, the fifteen-minute prediction, and the worker-survival band are other people's claims that we are reporting, not things we reproduced.

The thing worth sitting with: this is a bug where a generic scanner will flag "PHP version X, CVE present" and stop, with no way to tell whether a given deployment (mod_php versus php-fpm, process-manager mode, memory_limit) sits inside or outside the narrow exploitable band. Whether you are actually exploitable here is a question about your specific configuration, not your version string. That distinction is the entire difference between a vulnerability list and knowing what an attacker can really do with it.

How we tested it, so a skeptic can check our work

What this does not prove

A result without its limits is marketing, so here are the boundaries.

Frequently asked questions

What are CVE-2026-109618 and CVE-2026-109619?

Two critical remote-code-execution bugs in the PHP interpreter itself (CVSS 9.2), in the session.upload_progress upload-tracking feature. PHP shipped an emergency patch on 2026-10-21, with source backports even for the EOL 7.4, 8.0 and 8.1 branches. Because the flaw is in PHP core, it reaches any PHP app: Magento, Shopware, WooCommerce, WordPress, Laravel, Symfony and the rest.

Is there a working public exploit for php-fpm?

As of 2026-10-21 we could not find a published, generic, CVE-alone RCE against php-fpm. The public repos are each an unreproducible UAF claim, an explicit vulnerability probe rather than an exploit, or a chain that also needs an app-side bug. PHP's own advisory cites a demonstrated exploit but scopes it to mod_php, not php-fpm.

Why can't a WAF or input validation stop this?

The vulnerable code runs in PHP's native multipart request parser, before any application code executes. The trigger fires while PHP is still reading the body, so app-layer WAF rules and validation never get to see it.

Does the default php-fpm config protect me?

In our testing on stock official images the worker always exited after the OOM abort and the master spawned a fresh one, so the cross-request window never opened. But external research suggests worker survival depends on a narrow memory_limit band and process-manager tuning that stock images do not hit, so some configurations may differ. Patch regardless.

Should I patch EOL PHP like 7.4, 8.0 or 8.1?

Yes. PHP issued source backports for those branches even though most distros will not package a fix. Reconstructing a trigger from the patch diff is trivial, so "no published RCE yet" is not "safe."