Responding to php2shell, CVE-2026-109618 and CVE-2026-109619

Responding to php2shell, CVE-2026-109618 and CVE-2026-109619

Two critical PHP vulnerabilities, CVE-2026-109618 and CVE-2026-109619, leaked before PHP could ship a fix. Here's what's known, what isn't, and the three things we did to protect Report URI in the hours before the patch, all of which you can copy if you're running PHP in production too.

The problem

On 20th October, the PHP project announced an emergency security release for two critical vulnerabilities, scheduled for 21 October at 14:00 UTC. Each is rated CVSS v4.0 9.2. Normally that would leave everyone a day to get ready, but, unfortunately, the details of the bugs leaked online before the fix was out, along with claims of a working exploit called php2shell.

Report URI runs on PHP, so this one is definitely relevant to us, and we were putting processes in place to be prepared for the release of the patch. This post covers what's known, what isn't, and the three things we did to protect our servers and our customers' data while we waited for the patch.

What we know, and what we don't

Here's what we know for sure:

  • The bugs are real. watchTowr say they've reproduced them. PHP rate both as critical.
  • No exploitation in the wild had been reported at the time of writing. That's expected to change fast: mass exploitation is expected within minutes of the release, because the patch shows attackers exactly where the bugs are.
  • Only supported branches get fixes. That's PHP 8.2 and later. If you're on anything older, you're not getting a patch.

Here's what we don't know but what's been reported:

  • It's pre-authentication remote code execution in PHP itself. The bug triggers while PHP is reading the incoming request, before your application code runs. That means no login, no particular page, and nothing your code does can stop it.
  • The root cause hasn't been confirmed. One researcher has published a write-up claiming both CVEs are use-after-free bugs in PHP's session upload progress feature. It's the only detailed explanation available, and it's the one most of the advice below is built on. But another audit argues that one of those bugs can't happen in released PHP, and earlier reports placed the second bug in PHP's multipart parser instead. Until PHP publishes the fix, nobody outside the PHP team knows for sure.

We wanted protection that holds if the leaked explanation is right, and protection that still holds if it's wrong, so given the level of uncertainty, we decided to take some precautionary steps.

Step 1: Turn off session upload progress

PHP has a feature that most people have probably never heard of. If a form upload includes a field called PHP_SESSION_UPLOAD_PROGRESS, PHP writes "X of Y bytes received" into the user's session while the upload is still arriving, so a page can show a progress bar, and it's on by default.

The leaked write-up puts both bugs inside the code that handles this. We read that code ourselves, in the exact PHP version we run, and every line involved sits behind a single check: if the feature is switched off, PHP returns straight away and none of it runs.

So the first thing we did was switch it off, in the php.ini used by PHP-FPM and the CLI:

session.upload_progress.enabled = Off

Then restart PHP-FPM, and check that the running config picked it up. Ask php-fpm itself, not the php CLI, because they can read different ini files:

php-fpm8.5 -i | grep upload_progress.enabled

A few things worth knowing:

  • It won't break normal file uploads. This setting only controls the progress counter. Files still arrive in $_FILES exactly as before. We checked that our own upload features and inbound email webhooks kept working after the change.
  • It can't be turned back on with ini_set(). It's a "per directory" setting, so only ini files can change it. On Apache with mod_php that includes .htaccess, and on PHP-FPM it includes .user.ini files. Make sure none of those switch it back on.
  • Your application's session setup doesn't matter. This one surprised me because the upload progress code runs before your app does, using the session settings in php.ini, not the ones your framework sets later. With PHP's default settings, an attacker can just make up a session ID, send it in a PHPSESSID cookie, and PHP will create a brand-new session file for it. That works on any URL that reaches PHP, including endpoints that never use sessions.

That last point also gave us a cheap way to look for attempts. Our application stores sessions in Redis, so PHP's default session folder (/var/lib/php/sessions on Ubuntu) should always be empty. A sess_* file appearing there could only have come from this code path, and we found none. If your app doesn't use PHP's file-based sessions either, that folder is worth a look, though be aware that a clean-up job deletes old session files, possibly every 30 minutes, so it only tells you about recent activity.

Step 2: Block multipart requests at the edge

Step 1 is only as good as the leaked explanation. If that explanation is wrong, and the real bug is somewhere else in how PHP reads uploads, turning off upload progress does nothing.

What every version of the story has in common is the kind of request involved: multipart/form-data, the format browsers use for forms that upload files. PHP parses those bodies before any application code runs, so the only reliable place to stop one is before it reaches PHP at all.

All of our traffic comes through Cloudflare, and our servers refuse connections from anywhere else. That meant one rule at the edge could protect every server at once. If you're on Cloudflare, this WAF custom rule (action: Block) does it:

any(lower(http.request.headers["content-type"][*])[*] contains "multipart")

It lowercases the header so Multipart/Form-Data can't slip past, and checks every Content-Type header in case an attacker sends more than one.

We ended up doing the same thing in a Cloudflare Worker instead, because a Worker let us choose what each blocked request gets back:

addEventListener('fetch', event => {
  event.respondWith(handle(event.request))
})

async function handle(req) {
  const contentType = (req.headers.get('Content-Type') || '').toLowerCase()
  if (contentType.includes('multipart')) {
    return new Response('File uploads are temporarily disabled\n', { status: 403 })
  }
  return fetch(req)
}

Step 3: Don't lose data while you're blocking

Blocking every upload has a cost, and for us, it meant switching off two upload features in the dashboard, and stopping the inbound email webhook that delivers DMARC and TLS-RPT reports to our servers. Those reports arrive as multipart uploads.

We didn't want to just throw those reports away, and the good news is that our webhook provider will keep retrying them for 72 hours. Once we have the patch deployed, we can remove the temporary mitigation, and all customer data will be processed, resulting in no lost data.

When the patch lands

All of this is a stopgap measure designed as a pre-emptive fix. Until the patch is released, we won't know if these measures were effective but we feel it's better to take these steps out of an abundance of caution.

The real fix is to install the PHP patch as soon as it's released, which is scheduled for 14:00 UTC on 21st October, and we're ready to move the second it drops.

We'll update this post once PHP publishes the details, including whether our pre-emptive fixes were effective, and what additional steps we've taken, if any.

Update 21st Oct 2026 10:12 UTC

Cloudflare announced earlier that they have deployed a set of WAF rules to mitigate known exploit vectors for these vulnerabilities. The WAF changelog details the addition of these new rules. We can confirm that these rules are in place for Report URI traffic.

Update 21st Oct 2026 13:00 UTC

It's 1 hour until the patch is released and we've continued our work to ensure the protection of our infrastructure. This includes the following monitoring capabilities, which is not an exhaustive list but relevant to this issue:

  • PHP-FPM crashes: workers exiting on SIGSEGV or SIGABRT in the PHP-FPM logs, and segfault lines in the kernel log. Memory-corruption exploits often crash workers before they succeed.
  • PHP starting other programs: we check for PHP-FPM workers that have launched sh, bash, curl, wget, nc, python or perl, and for any long-running process under the web server user that isn't PHP-FPM.
  • New or changed files: we monitor /tmp, /var/tmp, /dev/shm and the application directory for new files. We also check that our deployed code matches what's in git, and look for any .user.ini file, any auto_prepend_file or auto_append_file setting, and any session file in PHP's default session folder.
  • Persistence: we look for crontabs or SSH keys belonging to the web server user, entries in /etc/ld.so.preload, and changes to cron, systemd units, sudoers or user accounts.
  • Network activity: we check for PHP-FPM connections to unexpected public IPs, and for new listening ports.

Update: 21st Oct 2026 14:12 UTC

PHP has released 8.5.12, 8.4.27, 8.3.36 and 8.2.35, along with an advisory. There are two bugs:

  • CVE-2026-109618 (OOM corruption): if PHP runs out of memory part-way through setting up a request, the cleanup afterwards can treat half-built data as valid.
  • CVE-2026-109619 (stale global state): internal state from one request can survive after its memory is freed. The next request handled by the same worker can then use it.

Session upload progress turned out to be one confirmed route to the second bug, so switching it off earlier gave us the protection we wanted. It isn't the only route, though, which is why patching is still the only complete fix. On top of what we'd already done, we've applied two more mitigations from the advisory:

  • fatal_error_backtraces = Off (PHP 8.5 only). This closes another confirmed route to the stale state bug.
  • memory_limit = -1, pinned with php_admin_value so no application or ini file can set a lower limit back.

Our web servers already cap request bodies at a suitable size so we didn't need to do that. We checked our servers' logs and went back 90 days looking for any signs of memory exhaustion and found none. We also checked for the compromise markers listed above and found nothing. The patched 8.5.12 build isn't in the PPA yet but as soon as it lands, we'll patch.

Update: 21st Oct 2026 18:22 UTC

We have begun the process of deploying the patched version of PHP 8.5.12 across the fleet after some initial testing showed good results.

Update: 21st Oct 2026 18:54 UTC

The update process is complete and all servers are now running PHP 8.5.12.

Incident Resolved: 21st Oct 2026 19:20 UTC

We're patched. Every one of our servers now runs PHP 8.5.12, which fixes both CVE-2026-109618 and CVE-2026-109619.

With the patch in place, we've removed the block on file uploads at our edge. Uploads work again, and the inbound email reports that were held up during the block are being redelivered and processed now, so nothing was lost.

Our configuration didn't expose the main way to trigger these bugs. PHP's memory limit has been disabled (memory_limit = -1) on our servers since 2021, and we've found no evidence of exploitation in our extensive checks detailed above, or after searching our Cloudflare WAF logs for malicious activity.

No action is required from our customers.