← Blog

PHP2Shell and Node.js: October 21 meant patching both, and here's how to do it without breaking anything

PHP has shipped 8.5.12, 8.4.27, 8.3.36 and 8.2.35 to close two critical flaws that allow unauthenticated code execution, and on the same day Node.js released 26.12.1, 24.22.1 and 22.23.4 with a Brotli decoder bug that also allows code execution. What we know, who's affected and what to do today.

A Wednesday to forget

On 21 October 2026, two of the platforms that power the most websites landed on the same day: PHP pushed out an emergency security release and, a few hours later, Node.js followed with its own. Social media was already calling it a "nightmare day" for anyone running servers, and that wasn't much of a stretch: LiteLLM, Next.js and several enterprise products had advisories out the same day too.

Here we focus on what touches almost everyone with a website or an API: PHP and Node.js. Let's get to it.

Updated 22 October. Word is going round that "the patch has already been bypassed". We looked into it and explain what we found further down, under Update, 22 October: what got bypassed were the temporary workarounds and some firewall rules, not the official patch.

In a nutshell

  • PHP: two critical flaws (CVE-2026-109618 and CVE-2026-109619, CVSS v4.0 9.2 each) let an attacker run code on the server without authenticating. You need 8.5.12, 8.4.27, 8.3.36 or 8.2.35, and you need to restart your PHP processes.
  • Node.js: ten flaws, four rated high. The worst (CVE-2026-111497) is an out-of-bounds write in the Brotli decoder that allows code execution. You need 26.12.1, 24.22.1 or 22.23.4.

PHP: what happened

There's a twist to this one. Details of the flaws leaked on 20 October, before any patch existed, and spread under the name "php2shell". That same day the PHP team confirmed two critical vulnerabilities and scheduled the release for the 21st at 14:00 UTC.

According to the official php.net advisory:

  • CVE-2026-109618 — code execution via out-of-memory corruption. If PHP runs out of memory halfway through setting up a request, it can be interrupted while internal data is only partly initialised, and the cleanup that follows treats that data as valid.
  • CVE-2026-109619 — code execution via stale global state. Internal data belonging to a request can survive after its memory has been freed. The next request handled by the same worker may use it before it gets reset.

Who's affected

PHP doesn't mince words: every PHP installation serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. It considers the risk highest with mod_php, where there's a demonstrated exploit in which an unauthenticated request triggers the attack before a single line of your application runs.

That last part matters: it doesn't matter how well built your site, your WordPress or your framework is. The bug is in PHP, not in your code.

What about older versions? PHP 8.1 and earlier no longer get updates from the project. The php.net advisory doesn't spell out which branches are affected, but Cloudflare, which took part in the coordinated response, says every server running PHP 5.4 or later. The security team has published minimal patches for 7.4, 8.0 and 8.1 for people who build from source, and some distributions and extended-support vendors are backporting them into their packages. If your server runs an unsupported PHP, assume you're affected until you can prove otherwise.

Is it being exploited?

This is where precision matters, because there are plenty of headlines going around:

  • Hadrian reports exploitation at scale since the morning of the 21st, before the patch was even out, mostly through PHP's session upload-progress feature (session.upload_progress).
  • watchTowr says it has seen exploitation attempts against both flaws in its honeypot network.
  • CISA's alert said it had not observed exploitation at the time of publication, and neither CVE was in its Known Exploited Vulnerabilities (KEV) catalog late on the 21st.

In plain terms: there are real attempts, and the gap between "the patch is out" and "it's being used against you" is measured in hours. Don't wait for official confirmation to patch.

PHP: what to do today

1. Check which version is actually running

php -v
php-fpm8.3 -v   # or whichever version you have installed

Careful: php -v tells you the command-line version, which isn't necessarily the one serving your site. If you use PHP-FPM or mod_php, check there too (a temporary phpinfo() page that you delete straight away, or the FPM binary itself).

In Docker:

docker exec <container> php -v

2. Update

  • Debian / Ubuntu with distribution packages: sudo apt update && sudo apt upgrade. Distributions usually backport the fix into their own version without changing the headline number, so don't look for "8.3.36" in php -v: look up your distribution's security notice (Debian Security Tracker, Ubuntu Security Notices) and confirm the installed package is the fixed one.
  • Third-party repositories (Ondřej Surý, Remi…): update the same way and check you've reached the patched version.
  • Docker with a floating tag (php:8.4-fpm, php:8.3-apache…): the official images are already updated. You need to rebuild pulling the new image:
    docker compose build --pull
    docker compose up -d
    
  • Docker with a pinned version (php:8.4.14-apache, for example): rebuilding won't help. You need to change the Dockerfile to the patched release of that branch.
  • Don't forget phpMyAdmin and the like: they ship PHP inside too. If it runs in a container, pull the new image and recreate the container (and while you're at it, ask yourself whether it really needs to be open to the internet).

3. Restart your PHP processes

This isn't optional. One of the two flaws lives precisely in state that survives between requests inside the same worker. Until you restart PHP-FPM or Apache, the old workers are still there.

sudo systemctl restart php8.3-fpm   # your version
sudo systemctl restart apache2      # if you use mod_php

Then check the version again. If you haven't checked it, it isn't done.

4. If you can't update today

PHP lists temporary measures that do not replace the patch:

  • For CVE-2026-109619: session.upload_progress.enabled = 0 and restart the workers. It's also the path most attacks are using. On PHP 8.5, add fatal_error_backtraces = 0. Bear in mind that PHP itself says the first setting blocks the confirmed attack paths but not other variants, and the second only blocks the confirmed path.
  • For CVE-2026-109618: memory-limit and request-size settings described in the advisory itself. Several researchers warn that this part was never a complete fix, so don't stop there.

PHP sums it up plainly: there are no complete mitigations, and updating is the only safe choice.

5. If your site sits behind Cloudflare, you already have a shield (with conditions)

Cloudflare got ahead of this one: since 03:02 UTC on 21 October, roughly nine hours before public disclosure, it has protected every customer using its web application firewall (WAF) against php2shell by default, Free plan included. On Free, the rules live in the Cloudflare Managed Free Ruleset under the name "Generic Rules - Anomalous Execution" and there's nothing to switch on; on paid plans they're in the Cloudflare Managed Ruleset.

The interesting part is how they did it. Looking at the request alone wasn't enough to tell an attack from legitimate traffic, so they launched a new capability that inspects the request and your server's response together, and can block the response before it reaches the attacker. They also say attackers kept tweaking their payloads to slip past the rules, and they kept adjusting them as they went.

Now the conditions, which are what decide whether this helps you or not:

  • The hostname has to be proxied (orange cloud). If the DNS record is grey, Cloudflare sees nothing.
  • Your server shouldn't accept direct connections to its IP. If someone knows the real IP and goes straight at it, Cloudflare is bypassed entirely. Ideally your server's firewall only lets in traffic from Cloudflare's IP ranges.
  • It doesn't replace the patch. Cloudflare says so itself: it's an extra layer while you update, not the fix.

And keep an eye out for PHP processes (php-fpm, apache2, httpd) spawning a shell or download tools such as curl or wget. If that happens, treat the server as compromised: isolate it, keep the logs, and look for web shells, new cron jobs and new user accounts.

Update, 22 October: has the patch been bypassed?

Since yesterday, social media has been saying the PHP patch "has already been bypassed". We checked, and as of today there's no evidence of that. Neither php.net nor the teams following the case closely (watchTowr, the Dutch NCSC, several security firms' live blogs) have reported a flaw in the patch, and php.net hasn't shipped any new release: the latest are still 8.5.12, 8.4.27, 8.3.36 and 8.2.35.

What has been bypassed, and probably where the confusion comes from, are three things that aren't the patch:

  • The temporary workarounds. Before the patch was out, plenty of people disabled session.upload_progress and called it done. PHP walked that back: it closes the known attack paths, not the variants. Anyone who stopped at that setting is still exposed.
  • Some firewall rules. Attackers have kept reshaping their requests to dodge WAF rules. For example, an earlier revision of some rules checked request size by looking at a header, and attackers got round it by sending the request in chunks, without that header. Vendors keep tightening the rules, but a firewall is an extra layer, not the fix.
  • Public exploits. More than a hundred GitHub repositories with proofs of concept have appeared, including a variant that gets round one of the operating system's memory protections (ASLR). That makes the attack easier against unpatched PHP. Against an updated PHP, nothing published works.

Why some "updated" servers are still vulnerable

This part is real, and it's what can make it look as if the patch doesn't work:

  • PHP wasn't restarted. The new package is installed, but PHP-FPM or Apache are still running the old processes. One of the two bugs lives exactly there.
  • Your distribution didn't have the fixed package yet when you updated, or it ships it under a version number that doesn't "look" new. Check your distribution's security advisory and look for the CVEs in the package changelog, not the number in php -v.
  • The Docker image was rebuilt too early, before the tag pointed at the fixed release. Run docker compose build --pull again and check the version inside the container.
  • Your PHP is out of support. PHP 8.1 and older have no official release: only minimal source patches to build yourself for 7.4, 8.0 and 8.1, and nothing for PHP 5.
  • The PHP comes bundled inside another product: a control panel, an appliance, a third-party image. Your apt upgrade won't touch that PHP; you depend on the vendor shipping its own update.

A head start on your inventory: the Dutch NCSC repository

The Netherlands' National Cyber Security Centre (NCSC-NL) has published a free GitHub repository, NCSC-NL/php2shell, with operational information on both flaws. The most useful parts:

  • Detection and mitigation material, to tell whether you're being attacked and what to apply while you update.
  • Lists of software and products that ship with PHP inside. That's exactly what the last point above calls for: finding out whether something you run brings its own PHP, so you know to wait for (or chase) the vendor's update.
  • Its advice matches ours: start with what's exposed to the internet, update or apply your distribution's patch, check bundled PHP in images and appliances separately, and watch your logs.

The NCSC itself says the repository isn't complete and is still being expanded, so it's worth checking back over the next few days.

Node.js: what happened

The same day, the Node.js project released fixes for all three supported lines: 26.x, 24.x and 22.x. They address ten vulnerabilities: four high, three medium and three low. The high ones:

  • CVE-2026-111497: out-of-bounds write in the Brotli decoder that allows code execution. Affects all three lines.
  • CVE-2026-105483 and CVE-2026-106069: two ways to bypass the hostname check on a TLS certificate (the first only on 26.x and 24.x).
  • CVE-2026-105482: dns.resolve truncates names containing a NUL character.

The release also updates dependencies with public vulnerabilities (c-ares, OpenSSL, SQLite, Undici) and stops trusting an eMudhra intermediate certificate following the compromise of that certificate authority.

Who's really exposed to the Brotli bug

The serious risk is when your application decompresses Brotli coming from outside: calls to zlib.brotliDecompress, compressed request bodies your server accepts, or responses you fetch with fetch from third-party servers. A Node process that receives nothing from the internet and only talks to your own services is at much lower risk. Lower isn't zero: update anyway.

One detail plenty of people miss: having nginx in front doesn't protect you. Nginx forwards the request and Node receives it just the same.

Unsupported lines get no patch

Node.js repeats it in every advisory: end-of-life versions are always affected when a security release comes out. If you're still on Node 18 or 20, there's no patch to apply: you need to move to a supported line.

Node.js: what to do today

1. Check which version is running

node -v
pm2 jlist | grep -o '"node_version":"[^"]*"' | sort -u   # if you use PM2
docker exec <container> node -v                          # if it runs in Docker

2. Update to the fixed release for your line

  • 26.x → 26.12.1
  • 24.x → 24.22.1
  • 22.x → 22.23.4

With nvm, nvm install 22.23.4 (or your line) and restart the app. With NodeSource packages, sudo apt update && sudo apt upgrade nodejs. In Docker, same as with PHP: with a floating tag (node:22-bookworm-slim), docker compose build --pull then up -d; with a pinned tag, change the Dockerfile.

3. Restart and verify

Node doesn't update in place: the running process stays the old one until you restart it (pm2 restart, systemctl restart or recreating the container). Afterwards, run node -v in the same environment the app runs in.

How we approach it

A day like this is won or lost before the patch comes out. What saves the most time is having an inventory: knowing which servers run PHP or Node, on exactly which version, whether it's Docker with a floating or pinned tag, and whether it's mod_php or PHP-FPM. With that in hand, "am I affected?" takes minutes to answer, and patching becomes a checklist rather than an afternoon of blind SSH sessions.

The other lesson is the usual one: unsupported PHP versions. A 7.2 or 7.4 that "works fine" doesn't get the official fix. Today it's php2shell; tomorrow it'll be something else. If you have one of those, the day to plan the migration was yesterday.

If you don't know which PHP or Node version your servers are running, or you've got one that's out of support and don't know where to start, let's talk.

References

Want to talk about your case?

Tell us what you need and we will get back to you within 24 hours with a clear proposal.

Get a quote