How to check whether Node2Shell can reach your app, update Node.js on every machine and platform that runs it, and close the two Next.js remote code execution holes from October 14
On October 21, 2026, Node.js shipped emergency releases for a flaw in its built-in Brotli decoder that the official CVE record scores 9.2 out of 10. The bug, tracked as CVE-2026-111497 and nicknamed Node2Shell, lets crafted compressed data write past the end of a buffer inside the runtime itself. The fixed releases are Node.js 22.23.4, 24.22.1 and 26.12.1, and the project ships nothing for Node.js 20 or any other end-of-life line - Node.js security release. A day later, researchers ran their own code on an official Node.js 24.22.0 image using nothing but Brotli data - Imperva Threat Research.
Here is the problem: no npm install fixes this. The vulnerable code lives inside the node binary, so a clean npm audit, a fresh lockfile and an up-to-date framework all leave it in place. Two days after the release, Qualys counted 3.3 million Node.js installations across the environments it scans, of which only 42,000 were patched and 1.7 million sat on end-of-life lines that will never receive the fix - Qualys. Managed platforms are not automatically ahead either. Vercel put free firewall rules in front of every project on day one, but says plainly that only the runtime patch covers responses your functions fetch from other servers - Vercel security bulletin. A throwaway function we deployed there at 14:20 UTC on October 24 still reported Node.js v24.21.0.
Node2Shell also landed one week after a separate Next.js emergency. On October 14, Next.js 15.5.28 and 16.4.1 fixed two critical unauthenticated remote code execution flaws scored 9.5, plus a denial-of-service bug in Server Actions - Next.js blog. One of those fixes depends on an image library version that an existing lockfile can quietly decline to install. And by October 23 the Brotli bug had spread well past Node: Python's Brotli and brotlicffi packages, Bun, Envoy and Istio all shipped fixes for the same CVE - Patch the Planet.
This guide explains what Node2Shell actually is in plain language, how to tell whether attacker-controlled data can reach the vulnerable decoder in your app, and exactly how to update Node.js on macOS, Windows, Linux, Docker, CI runners and hosting platforms such as Vercel. It covers the Next.js patch you should ship the same week (including the lockfile trap), your real options if you are still on Node.js 20, the stopgaps that help while you patch and the gaps they leave, and how to check afterwards that the fix is actually running. It ends with the structural question this month raises for anyone shipping software: when finding bugs gets cheap, the speed at which you can ship a patch becomes the security model.
Contents
- What Node2Shell Is, in Plain Terms
- How It Unfolded: A February Fix, an October Disclosure
- Are You Exposed? The Paths Into the Decoder
- How Exposed Everyone Is: The Numbers
- Step 1: Find Every Node.js You Run
- Step 2: Update Node.js, Environment by Environment
- Step 3: Patch Next.js the Same Week
- Node.js 20 and Other End-of-Life Lines: Your Real Options
- If You Cannot Patch Today: Stopgaps and Their Limits
- Beyond Node: Python, Bun, Envoy, Istio and the HTTP2Shell Spread
- After the Patch: Verify, Restart, Look Back
- The Bigger Lesson: Patch Speed Is the Security Model Now
- Conclusion: A Decision Framework for This Week
Every Way to Deal With Node2Shell, Scored
Before the detail, here is every realistic response a team running Node.js has this week, scored on the four things that matter. Most teams will combine two of them: a stopgap within the hour and the real fix within a day. Read the table as a map of where each option belongs in that plan, not as a menu where you pick one.
The scores are judgments built from the evidence cited in the cells and in the sections below. A "Closes both paths" score of 10 means the option fixes the decoder for inbound request bodies, fetched responses and files alike; anything that only filters traffic scores low there no matter how convenient it is.
| # | Approach | What It Does | Closes both paths (40%) | Effort today (20%) | Break risk (20%) | Lasts (20%) | Final |
|---|---|---|---|---|---|---|---|
| 1 | Patch in place | Move to 22.23.4, 24.22.1 or 26.12.1, rebuild, restart | 10 - the official fix for the decoder on every path | 8 - a version bump plus image rebuilds and restarts | 9 - same release line, security-only changes | 7 - 22.x support ends April 30, 2027 | 8.8 |
| 2 | Upgrade off Node 20 | Jump an end-of-life app to 24 LTS (or 26 LTS from Oct 28) | 10 - lands on a fixed release | 6 - needs a test pass and native module rebuilds | 6 - two major versions of changes at once | 10 - 24.x supported to April 2028, 26.x to April 2029 | 8.4 |
| 3 | Wait for your platform | Let Vercel, Lambda or another host roll its runtime | 5 - the real fix, but only when it lands; Vercel 24.x still v24.21.0 on Oct 24 | 10 - nothing to do | 9 - same major line | 8 - future patches arrive the same way | 7.4 |
| 4 | Paid extended support | Buy patched builds of an EOL line (HeroDevs NES) | 7 - a real fix for 20.x, but still "in progress" on Oct 23 | 6 - procurement plus a binary swap | 9 - same version, same behaviour | 5 - you pay for every future fix | 6.8 |
| 5 | Reject Brotli request bodies | Return 415 for Content-Encoding: br before parsing | 4 - closes the inbound path only | 8 - one middleware or proxy rule | 8 - few clients send Brotli bodies | 3 - a stopgap by design | 5.4 |
| 6 | WAF rules only | Rely on Vercel's free rules or Imperva's | 3 - known inbound patterns only, no fetched responses | 10 - automatic on Vercel | 9 - rare false positives | 2 - attackers mutate payloads | 5.4 |
| 7 | Guard outbound fetches | Refuse Brotli responses from servers you do not control | 4 - closes one path, hard to cover every library | 4 - client by client changes | 6 - some APIs only answer in Brotli | 3 - temporary by intent | 4.2 |
| 8 | Turn off Next.js image optimization | Set images.unoptimized: true while unpatched | 2 - closes one fetch path in self-hosted Next.js | 9 - one config line and a redeploy | 5 - larger images, slower pages | 2 - a pause, not a plan | 4.0 |
How the criteria were weighted. "Closes both paths" carries 40% because it is the only column that changes whether an attacker can reach the bug; everything else is cost. "Effort today" (20%) reflects that this is an hours-scale emergency with public proof-of-concept research already out. "Break risk" (20%) captures the real reason teams stay on old runtimes: fear of a broken build on a Friday. "Lasts" (20%) matters because the next Node.js security release is a matter of weeks, not years, and any choice that strands you on an unsupported line just moves this problem to next month.
The ranking says something simple: the boring answer wins. Updating the runtime inside the release line you already use is the highest-scoring move for almost everyone, and the stopgaps in rows 5 to 8 exist to cover the hours between now and that update, not to replace it. The two rows that surprise people are row 3 and row 2. Waiting for a managed platform feels safe because it costs nothing, but it scores only middling on coverage because the platform decides when the fix arrives; section 6 shows how far behind one of them still was on October 24. And upgrading off Node.js 20 scores nearly as high as patching in place, because for an end-of-life line there is no "in place" to patch.
1. What Node2Shell Is, in Plain Terms
Most web traffic today is compressed before it travels. Brotli is one of the formats that does it: a compression scheme Google released for web content, which servers advertise with the Content-Encoding: br header and browsers, CDNs and HTTP clients unpack on arrival. Node.js carries its own copy of Google's Brotli decoder, written in C, inside the node binary. Your JavaScript reaches it through the node:zlib module, and so does a lot of code you did not write: the built-in fetch() unpacks Brotli responses automatically, and several popular body parsers unpack Brotli request bodies before your route handler ever sees them - Aikido Security.
The bug is a bookkeeping mistake in that decoder. A Brotli stream is split into blocks, and the stream can tell the decoder to switch between block types. When the switch instruction was invalid, the decoder treated it as "I need more input" instead of "this stream is broken". In the Node.js team's words, the streaming decoder "kept appending input bytes to an eight-byte internal buffer", which means an attacker who can make your app decompress their data can write past the end of that buffer and corrupt process memory. The result can be a crash, a leak of memory contents, or remote code execution - Node.js security release.
That last outcome is why the name ends in "Shell". In security slang, getting a shell means running your own commands on someone else's machine. The naming habit started with React2Shell in December 2025, when a flaw in React Server Components let crafted requests run code on servers built with Next.js - Vercel React2Shell bulletin. This October brought PHP2Shell on October 21 (covered in our PHP2Shell patching guide), Node2Shell the next day, and, once the same Brotli bug showed up in Python libraries, HTTP2Shell on October 23 - Aikido Security on the Python spread. The Node2Shell name itself came from Imperva researcher Ron Masas - Cyber Kendra. Semgrep's researchers call the name "somewhat misleading", since the defect sits in the bundled Brotli library rather than in Node's own code, and since it is not a problem with any npm package named brotli - Semgrep.
Imperva's research team drew the clearest picture of how data reaches the bug. Their figure shows the two doors into the same decoder: a compressed response consumed by fetch(), and an inbound request body decoded by Express 5 and body-parser 2. Both lead to the vulnerable code on Node.js 24.22.0 and to a clean rejection on 24.22.1.
The figure carries the most important practical point of this whole guide: the attacker need not be your user. On the left-hand path, the malicious Brotli data comes from a server your app chose to call, which could be a webhook target, a link preview, a scraped page or a third-party API that has itself been compromised. A firewall in front of your app never sees that traffic, because your app opened the connection. That is why every vendor that shipped a firewall rule this week, Vercel and Imperva included, also tells you to update the runtime.
Severity ratings for Node2Shell differ, and the difference is informative rather than confusing. Node.js labels the bug High. Google, the CVE Numbering Authority for Brotli, scored it 9.2 Critical under CVSS 4.0 with the vector AV:N/AC:L/AT:P/PR:N/UI:N, which reads as: reachable over the network, low complexity, no login, no user interaction, but with an attack requirement present - CVE record. That "AT:P" is the honest caveat. The attacker needs your app to decompress Brotli they control, which most apps do somewhere but not every app does on a public route. The CVE record's own description covers every product built on Google Brotli 1.0.3 through 1.2.0, and names the fixed library releases as 1.0.10, 1.1.1 and 1.2.1.
The bug was found by OpenAI Codex Security, credited to researchers Khai Tran and Sylvie Mayer, and fixed in Node.js by maintainers Matteo Collina and Antoine du Hamel - Node.js security release. It reached the public through Patch the Planet, an initiative OpenAI runs with Trail of Bits under its Daybreak security program to pair AI-assisted vulnerability research with human review and coordinated fixes - OpenAI. That origin matters for section 12, because it is a preview of how most serious bugs will be found from now on.
Why this matters for you, in one sentence: the flaw sits below every layer you normally patch, so it is fixed only when the binary that runs your code is replaced and restarted. How to apply it: treat "which node binary is running in production right now" as the question to answer today, and treat your framework and dependency updates as a separate job that does not touch this bug at all. The next section shows how the week unfolded, which explains why so many teams were caught between two fixes.
2. How It Unfolded: A February Fix, an October Disclosure
The Node2Shell fix is older than the disclosure by eight months. The upstream correction to Google's Brotli decoder was committed on February 27, 2026 - Cycode. Why the gap was so long has not been explained publicly, but the shape of the problem accounts for much of it: the decoder ships inside Node.js, Bun, Python bindings, proxies such as Envoy, command line tools such as curl, font tools, and every Electron app that embeds a browser and a Node runtime side by side, and each of those needed its own release. Coordinated disclosure is a trade-off. It gives vendors time to prepare releases, but it also means a fix can exist in public source code for months before anyone downstream is told to install it.
The second thread of the story started in June. On June 22, 2026, Trail of Bits announced Patch the Planet with OpenAI, committing its security research organization to finding and fixing bugs in critical open source projects with AI assistance - Trail of Bits. The initiative's list of recent disclosures shows two entries: the PHP flaws behind PHP2Shell and the Brotli decoder bug - Patch the Planet. Both landed in the same week, which is why many teams ended up patching two runtimes at once.
Meanwhile the Next.js team was running its own emergency on a parallel track. On October 8 it pre-announced an out-of-band release for three upstream vulnerabilities, two critical and one high, two of them postponed from the September release because of upstream coordination delays - Next.js pre-announcement. That release shipped on October 14 as 15.5.28 and 16.4.1. The same day, Node.js shipped regular feature releases 24.22.0 and 26.12.0, and those carry no Brotli fix, so anyone who updated "to the latest" that week landed on a vulnerable build that looked brand new.
The week of October 19 then compressed into four days. The table below is the short version, and every date in it is sourced in the paragraphs around it.
| Date | What happened |
|---|---|
| October 21 | Node.js ships 22.23.4, 24.22.1 and 26.12.1, fixing ten vulnerabilities, four rated high; Vercel deploys firewall rules for every project |
| October 22 | Google publishes the CVE record at 13:35 UTC; Imperva demonstrates code execution; Python's Brotli 1.2.1, brotlicffi 1.2.1.0 and Bun 1.4.4 ship |
| October 23 | Aikido renames the spread HTTP2Shell; Patch the Planet adds Envoy and Istio fixes; a Vercel customer reports the managed runtime still on v24.21.0 |
| October 24 | Our own throwaway Vercel deployment still reports v24.21.0 on 24.x and v22.23.2 on 22.x |
| October 28 | Node.js 26 becomes LTS and the Node.js 27 alpha begins |
The Docker official images for all three fixed releases were published at about 02:40 UTC on October 22 - Docker Hub, and the CVE record shows Google reserved the identifier on October 16 and published it on October 22 - CVE record. Python's Brotli 1.2.1 reached PyPI at 13:57 UTC on October 22 - PyPI, and brotlicffi 1.2.1.0 followed at 16:19 UTC - PyPI. Express's body-parser also shipped 2.4.0 and 1.20.9 on October 22, for a different but related problem: a crafted compressed body with trailing bytes could crash the whole process - body-parser advisory. Even the Vercel community thread asking why a redeployed app still reported node=v24.21.0 had no staff reply when we read it - Vercel Community.
For a spoken walkthrough of the week, the most-watched explainer so far comes from Academind, whose 12-minute video published on October 22 covers both PHP2Shell and the Node.js release under the blunt title "PHP, Node and more are vulnerable! You must act NOW!". It had close to 96,000 views within two days, far ahead of any other video on the topic, and it is a good thing to send to a teammate who needs the urgency explained before they will read a long guide.
The timeline teaches two practical lessons. First, "latest" is not "patched": the October 14 feature releases were the newest Node.js builds for a week and still carried the bug, so a version check needs a specific number, not a feeling of freshness. Second, a coordinated disclosure gives you a short window, not a long one. Semgrep's write-up cites Rapid7 data putting the recent time to exploitation for high-severity flaws at roughly three days, and a Nucleus analysis putting the median at 5.5 days from public proof of concept to listing in the US government's exploited-vulnerabilities catalog - Semgrep. Public proof-of-concept research for Node2Shell appeared within a day of the release. How to apply both lessons: write the target version numbers into your ticket, and plan to finish the runtime update this week rather than in the next sprint. With the timeline clear, the next question is whether attacker data can actually reach the decoder in your app.
3. Are You Exposed? The Paths Into the Decoder
Exposure to Node2Shell takes two things at once: a vulnerable runtime and a path for a stranger's Brotli data to reach the decoder. The runtime half is simple, because every Node.js 22, 24 and 26 build before this week's releases is affected, and so is every end-of-life line. The path half is where teams waste time, because the decoder hides behind libraries. Exposure exists when your code, or a library it uses, decompresses Brotli data an attacker controls, whether that data arrives in a request body or in a response you fetched - Vercel security bulletin. Receiving a compressed request is not enough on its own: a bare Node.js HTTP server does not decode incoming Brotli bodies just by receiving them - Imperva Threat Research.
That gives three doors to check. The inbound door is any middleware that inflates compressed request bodies before your handler runs. The outbound door is any code that calls another server and lets the HTTP client unpack the answer. The file door is anything that decompresses stored data, such as uploads, archives or fonts, since the WOFF2 web font format and plenty of data formats use Brotli internally. Patch the Planet's advisory makes the same point in its own words: exposure "is not limited to HTTP traffic", and compressing only outgoing data does not by itself expose you - Patch the Planet.
The inbound door: body parsers that inflate Brotli
The most common inbound path runs through Express 5. Its body parser, body-parser 2.x, supports "automatic inflation of gzip, br (brotli) and deflate encodings" in its JSON, raw and text parsers - body-parser documentation. Express 5 is what npm install express gives you today, with npm's latest tag pointing at 5.3.0. So the one-line app.use(express.json()) that sits at the top of countless tutorials, mounted globally, hands Brotli bodies from any client on the internet to the decoder before routing even happens. Imperva's proof of concept used exactly this path with Express 5.0.0 to 5.3.0 and body-parser 2.0.0 to 2.3.0.
The same pattern shows up in other frameworks. Semgrep's exposure table lists the Koa family (koa-bodyparser, @koa/bodyparser, koa-body and co-body, plus frameworks built on them such as Egg and Strapi), Fastify when @fastify/compress is registered with request decompression, and ultimate-express - Semgrep. The older stack is the exception: Express 4 with body-parser 1.x does not decode Brotli request bodies - Cycode. That is not a reason to stay on Express 4, but it does change how urgent the inbound door is for a legacy app.
| Component | Decodes untrusted Brotli? | Path | What to do |
|---|---|---|---|
| Express 5 / body-parser 2.x | Yes, by default | Inbound bodies | Patch Node; also update body-parser to 2.4.0 |
| Koa body parsers (koa-body, co-body, @koa/bodyparser) | Yes | Inbound bodies | Patch Node |
| Fastify + @fastify/compress | Yes, with request decompression | Inbound bodies | Patch Node |
Built-in fetch() | Yes, automatically | Fetched responses | Patch Node; WAF rules do not help |
Direct node:zlib Brotli calls | Yes, on whatever you pass | Any | Patch Node; audit the inputs |
| Express 4 / body-parser 1.x | No Brotli request bodies | Inbound bodies | Patch Node anyway (fetch() is still exposed) |
Bare http server | No | None by itself | Patch Node anyway |
The table makes the asymmetry visible. Every inbound row has a stopgap available at the edge, because a proxy or firewall can see and refuse a Content-Encoding: br header on the way in. The outbound and direct rows have no edge to filter. They are fixed only by the runtime update, which is why the vendors who shipped firewall rules all ended their advisories with the same instruction to upgrade.
The outbound door: responses you fetch
The outbound door is the one most teams underestimate. Node's built-in fetch() decompresses Brotli responses automatically, so any server your code calls can deliver the malicious data - Aikido Security. "Any server your code calls" is broader than it sounds in 2026. It covers webhook delivery and verification, link previews and unfurling, RSS and sitemap readers, scrapers, payment and email providers, AI agent tools that browse the web, and MCP servers that fetch URLs on a model's behalf, a pattern our guide to shipping an MCP server covers from the builder's side. If your product lets a user paste a URL that your server then fetches, the user chooses the server, and so the user chooses the bytes.
Self-hosted Next.js has its own version of this door. The built-in image optimizer fetches remote images from the hosts you allow and decodes them on your server. Vercel's bulletin states directly that Next.js does not block Brotli responses in image optimization and that upgrading Node.js is the fix - Vercel security bulletin. Patch the Planet offers images.unoptimized: true as a temporary switch, and notes that it does not protect other server-side fetches, custom loaders or other decoders - Patch the Planet. Your own Server Components and route handlers that call third-party APIs with fetch() are exposed in exactly the same way, on any host.
A two-minute exposure check
The decision is short enough to draw. The diagram below walks the questions in order, and each branch points to the section that handles it.
To answer the second question with evidence instead of memory, run three searches in each repository. The first lists the parsers that open the inbound door; the second finds direct decoder calls; the third finds outbound fetches whose target you should then judge by hand.
# Inbound: which body parsers are in the tree (Express 5 pulls body-parser 2.x)
npm ls express body-parser koa koa-body co-body @fastify/compress 2>/dev/null
# Direct decoder use in your code (skip node_modules, check vendored code separately)
grep -rnE "brotliDecompress|createBrotliDecompress|BrotliDecompress" --include=*.{js,ts,mjs,cjs} . | grep -v node_modules
# Outbound: every fetch, then judge whether a stranger can choose the URL
grep -rnE "\bfetch\(" --include=*.{js,ts,tsx,mjs} . | grep -v node_modules | wc -l
Why this matters: the urgency of your patch depends on these answers, and so does the usefulness of every stopgap in section 9. A team whose only exposure is inbound through Express 5 can buy real time with a one-line middleware guard; a team whose app fetches user-supplied URLs cannot, because nothing in front of the app sees that traffic. How to apply it: record the answers in the ticket next to the runtime version, because they decide whether a stopgap goes in within the hour or the runtime goes in within the hour. If you build with an AI coder, this is also a good moment to read our guide to securing an AI-built app before launch, since generated code tends to reach for fetch() on user input without a second thought.
4. How Exposed Everyone Is: The Numbers
It is tempting to treat an advisory like this as somebody else's problem, so it helps to look at how much unpatched Node.js is actually running. The best snapshot so far comes from Qualys, which inventories software across the environments its customers scan. Two days after the release it counted 3.3 million Node.js installations, with 15% on internet-facing assets. Of those, 1.3 million ran a supported line that had not been patched yet, 1.7 million ran end-of-life lines such as 25, 23 and 20, and only 42,000 were already on a fixed release - Qualys.
The striking number in that set is not the small patched count, which is to be expected 48 hours after a release. It is that end-of-life installations outnumber unpatched supported ones. For those 1.7 million, there is no patch to apply at all, only an upgrade to plan.
Download data tells the same story from another angle. Node.js 20 reached end of life on April 30, 2026, yet in July it was still downloaded more than 103.7 million times, against roughly 358.8 million for Node.js 24 and 283.9 million for Node.js 22 - NodeSource. Node.js 18, end of life since April 2025, added about 32.7 million - NodeSource. Downloads are not running servers, and the companies publishing these figures sell support for old versions, so treat the framing with care. The likeliest explanation is mundane: CI pipelines and Dockerfiles that still say node:20 pull it again on every fresh build.
The third number is the one that should change how you plan, not just how you react: the pace of Node.js security releases has jumped. Counting the vulnerabilities listed in each official release post, the three security releases of 2025 fixed 8 issues between them. The five security releases from January 13 to October 21, 2026 fixed 50, with four rated high in this month's release alone - Node.js vulnerability reports. The December 2025 batch shipped on January 13, 2026, so it is labelled by its release month in the chart below.
Now the honest other side, because a guide that only frightens is not useful. As of October 23, nobody had reported Node2Shell being exploited in the wild, and the CVE was not in CISA's Known Exploited Vulnerabilities catalog - HeroDevs. We checked the catalog again on October 24 and its October 23 edition still did not list it - CISA KEV catalog. Imperva's code execution worked on an unmodified official image but "took multiple attempts" in their setup, on Linux arm64 - Cyber Kendra. The CVSS vector itself admits an attack requirement. So the realistic range runs from "a crash you can trigger at will", which is already a denial of service on any exposed endpoint, to "code execution with some effort", which is the outcome that turns into stolen API keys and database credentials.
Why these numbers matter: they show you are not late if you patch this week, but they also show that the default outcome for an unattended app is to stay vulnerable, because the patched share two days in was about one installation in eighty. How to apply them: put the end-of-life question first in your planning, because it is the slow part, and treat the patch itself as routine work that should fit in a day. The next three sections walk through that work in order: find every Node.js, update it, then patch Next.js.
5. Step 1: Find Every Node.js You Run
Most teams underestimate how many copies of Node.js they run, because each copy arrived through a different door. There is the one on your laptop, the one your CI runner downloads, the one baked into every Docker image you built in the last year, the one your hosting platform supplies to serverless functions, and the ones hidden inside tools: Electron desktop apps, single-executable apps, and binaries compiled with Bun. Each of those copies has its own Brotli decoder and its own update path. The vulnerable one is whichever copy decodes attacker data, and that is usually the one you think about least.
The goal of this step is a short inventory with a version number next to each line. It does not need to be fancy. A list of "where, which version, who updates it" turns the patch from a vague worry into a checklist, and it is the same list you will want the next time a runtime advisory lands in six weeks.
Where Node.js hides
The diagram's three groups matter because they have three different owners. Anything in the first group is yours to update today. Anything in the second group is updated by your host on its own schedule, and the most you can do is check, redeploy and pick a supported major version. Anything in the third group is fixed only by rebuilding the artifact with a patched runtime and shipping it again, which is easy to forget because nobody thinks of a desktop app as "running Node.js". Patch the Planet specifically warns that Electron apps can hold more than one Brotli copy, one in Chromium and one in Node.js, so a fixed browser component does not prove the embedded runtime is fixed - Patch the Planet.
The commands that tell you the truth
On any machine where you can open a shell, one line prints the version that matters. In a container image, run the same check inside the image rather than trusting its tag, because a tag like node:24 points at whatever was current when the image was built. In production, the most reliable check is the one the running process makes about itself: log process.version at startup. Vercel's own documentation recommends exactly that, or running node -v in the build command - Vercel Node.js versions.
# The running binary on this machine
node -p "process.version"
# Inside each container image you deploy (repeat per image)
docker run --rm your-registry/your-app:prod node -p "process.version"
# In app code, once at startup, so production logs prove what is running
console.log(` [runtime] node ${process.version} pid ${process.pid}`)
There is one trap in this step that we measured ourselves on October 24. You might expect process.versions.brotli to tell you whether the decoder is fixed. It does not. The official Node.js 24.22.1 binary still reports brotli=1.2.0, and the official 22.23.4 reports brotli=1.1.0, because Node.js fixed the bug by cherry-picking the upstream commit into its existing copy rather than upgrading the library - Node.js 24.22.1 release notes. The Node.js version number is the signal for official builds. The Brotli string is noise there.
The trap runs the other way on Linux distribution packages, where node often links the operating system's Brotli library instead of bundling its own. Debian's security tracker notes that "nodejs uses system provided brotli library", lists trixie's Node.js 20.19.2 as vulnerable, and ships the actual fix as a brotli package update through DSA-6582-1 for trixie and DLA-4855-1 for bookworm - Debian security tracker. On such a system, a Node.js version that never changes can become safe the moment the library package updates, and a "new enough" Node.js can stay unsafe if the library does not. To see which kind of binary you have, check what it links:
# Linux: does this node link the system Brotli library?
ldd "$(command -v node)" | grep -i brotli || echo "bundled Brotli (official build): check the Node.js version"
# macOS (Homebrew builds link Homebrew's brotli)
otool -L "$(command -v node)" | grep -i brotli || echo "bundled Brotli: check the Node.js version"
Why this step matters: every later step is only as good as this inventory, and the version that matters is the one in memory serving requests, not the one in a config file. How to apply it: create one line per environment with the observed process.version, the owner, and how that copy gets updated, then move on to updating each line in the next section.
6. Step 2: Update Node.js, Environment by Environment
The fix itself is undramatic: install 22.23.4, 24.22.1 or 26.12.1, or anything newer in the same line, then restart everything that runs on it. The only real decision is which line. If you are already on 22, 24 or 26, stay on it for today and patch in place; a security update is not the moment to change major versions too. If you are on 20 or older, or on the odd-numbered 23 or 25, there is no patch for your line, and section 8 covers the move. Every official download for the fixed releases, with checksums, is listed in the release notes - Node.js 24.22.1 release.
The commands differ by how Node.js was installed, and mixing methods is the most common way to "upgrade" and keep running the old binary. If which node points at an nvm directory, update through nvm; if it points at Homebrew, update through Homebrew; and so on. After each update, open a new shell and re-run node -p process.version before you believe it.
macOS
On a Mac, three install methods cover almost everyone. The official .pkg installer from nodejs.org replaces the system-wide copy. Homebrew carries the fixed releases already: the node formula is at 26.12.1, node@24 at 24.22.1 and node@22 at 22.23.4 - Homebrew formulae. Homebrew's node@20 formula is deprecated and scheduled to be disabled on October 28, 2026 - Homebrew node@20 formula. For per-project versions, nvm handles it in two commands - nvm.
# Homebrew: current release line (26.x)
brew update && brew upgrade node
# Homebrew: stay on an LTS line instead
brew install node@24 && brew link --overwrite --force node@24
# nvm: install the patched LTS and make it the default for new shells
nvm install 24 && nvm alias default 24
node -p "process.version" # expect v24.22.1 or newer
The most common Mac failure is having two of these at once. A developer installs Node.js from the .pkg years ago, later adds Homebrew and nvm, and the shell quietly picks whichever directory comes first on the PATH. Run which -a node after the upgrade: if it lists more than one binary, every one of them is a copy to patch or remove, and the first line is the one your terminal actually runs. Editors, git hooks and background tools launched from the Dock often see a different PATH from your terminal, so a patched shell does not prove a patched editor. Restart the editor and any local dev servers after the upgrade, then check the version from inside the tool that runs your code.
Windows
On Windows, the official .msi installer from the release page is the simplest path for a single machine. For managed fleets and scripts, the Windows Package Manager carries the LTS release as OpenJS.NodeJS.LTS, whose manifests already include 24.22.1 - winget package manifests. Developers who switch versions per project usually use nvm-windows, which is a separate project from nvm with its own commands - nvm-windows.
# Windows Package Manager
winget upgrade OpenJS.NodeJS.LTS
# nvm-windows
nvm install 24.22.1
nvm use 24.22.1
node -p "process.version"
Windows has the same duplicate-install trap as macOS, with one extra twist. The nvm-windows README recommends uninstalling any earlier Node.js before using it, and warns that if you do not, nvm use "may appear to do nothing at all" while node -v keeps showing the original version - nvm-windows README. That is exactly how a patch looks applied and is not. Run where node in a fresh terminal to see every copy on the PATH. Windows services and scheduled tasks that run Node.js keep their own path to the binary, so update their configuration if they point at a versioned folder, then restart them. Build agents on Windows deserve the same check as Linux CI runners in the subsection below.
Linux servers and VMs
On Linux, where Node.js came from decides the fix. If you installed from NodeSource, its repository already serves 24.22.1 for the 24.x line, and a normal package upgrade picks it up - NodeSource distributions. If you used the distribution's own nodejs package, follow your distribution's advisory, which may fix the system Brotli library rather than Node.js itself, as Debian did. If you used nvm or a tarball, reinstall the patched release the same way.
# NodeSource (Debian/Ubuntu): refresh the repo for your line, then upgrade
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
node -p "process.version"
# Debian/Ubuntu distro packages: the fix may arrive as a library update
sudo apt-get update && sudo apt-get install --only-upgrade libbrotli1 nodejs
On servers, the package upgrade is only half the job. A systemd service, a PM2 process list or a supervisor config keeps running the old binary until it restarts, so finish with an explicit restart of every unit that runs Node.js and then check the version from inside the service, not from your SSH session. If the server runs several apps under different Node.js versions through nvm, each app's start script decides which binary it uses, so read the start scripts rather than assuming the default alias applies. For Red Hat family systems, NodeSource publishes the same setup scripts on its RPM host, and the distribution's own nodejs packages follow the vendor's own advisories.
Docker images
Containers are where unpatched runtimes survive longest, because an image is a frozen copy of whatever was current on the day it was built. The official images for the fixed releases were published on Docker Hub on October 22, in Debian bookworm, Debian trixie and Alpine variants - Docker Hub. The node:20 tag, by contrast, was last updated on April 22, 2026 and will not move again. Pin the patched version explicitly so the fix cannot silently regress, rebuild with a fresh pull, and check inside the result.
# Before: floating, and frozen at whatever day the image was last built
# FROM node:24-bookworm-slim
# After: an explicit patched release (or pin by digest for full reproducibility)
FROM node:24.22.1-bookworm-slim
docker build --pull --no-cache -t your-app:patched .
docker run --rm your-app:patched node -p "process.version" # v24.22.1
Two details decide whether the rebuild worked. The --pull flag forces Docker to fetch the current base image instead of reusing a cached layer from last month, and --no-cache stops it from reusing your own layers built on the old base. Multi-stage builds need the patched version in every FROM line that ends up in the final image, not just the build stage. If you pin by digest for reproducibility, which is a good habit, remember that a digest never moves on its own: you must update it deliberately whenever a security release lands, and a tool such as Dependabot or Renovate can open that pull request for you. Then redeploy, because a rebuilt image in a registry protects nothing until the orchestrator replaces the running containers.
CI runners
CI is the copy most teams forget, and it matters more than it looks: it runs your build scripts, your tests and sometimes your deploy credentials. GitHub's actions/setup-node, now at v7, resolves a version spec like 24 against the runner's local cache first and downloads only when the cache has no match, so a runner image can keep handing you an older patch release for a while. The README's check-latest input exists for exactly this: it makes the action check for the newest version that satisfies your spec - actions/setup-node.
- uses: actions/setup-node@v7
with:
node-version: 24
check-latest: true # do not settle for an older cached 24.x
- run: node -p "process.version"
The final run line costs nothing and makes every build log show the exact runtime, which turns "did CI pick up the patch" into a search instead of a guess. Self-hosted runners need more care than hosted ones, because their tool cache survives between jobs and can serve an old version indefinitely; clear the cache or pin the exact patched version there. CI matters for a second reason beyond tests: build steps often fetch packages, fonts and data from the internet, and a CI job holding deployment credentials while running an unpatched decoder is a target worth taking seriously. The same advice applies to GitLab, CircleCI and other systems that select Node.js through an image tag or a version file: pin the patched version and print it.
Vercel and other managed platforms
On a managed platform you do not install Node.js at all; you pick a major line and the host chooses the patch. Vercel currently offers 24.x (the default) and 22.x, and its documentation says it "automatically rolls out minor and patch updates when needed, such as to fix a security issue" - Vercel Node.js versions. Its Node2Shell bulletin says projects on 22.x or 24.x need no settings change, while projects on 20.x must switch to 22.x or 24.x and redeploy - Vercel security bulletin.
The catch is timing. At 14:20 UTC on October 24, three days after the release, we deployed two throwaway functions on Vercel that print process.version and deleted them right after. The 24.x function reported v24.21.0, a release from early September - Node.js 24.21.0 release, and the 22.x function reported v22.23.2, the July security release - Node.js 22.23.2 release. Both are below the fixed releases, and neither is even the newest non-security build of its line. Vercel's bulletin says patched versions are being rolled out without giving a date, and a customer asking the same question on October 23 had no answer when we read the thread - Vercel Community. Vercel has not said whether it backports fixes without changing the version string, so the safe reading is "unpatched until the number moves".
What you can do on a managed platform is limited but real. Keep the free firewall rules, which do cover inbound bodies. Move any project still set to 20.x. Add a startup log line with process.version so you can see the day the patch lands, then redeploy, since a running deployment keeps the runtime it started with. For outbound fetches to servers you do not control, consider the guards in section 9 until the number moves. AWS Lambda, Netlify and other function hosts work the same way in principle; we found no Lambda runtime announcement specific to this CVE as of October 24, so the startup log line is your evidence there too. If you are choosing where to host next, our ranking of where to deploy your app compares who owns which layer on each platform.
Bun, and restarting everything
If you run Bun, update to 1.4.4, which includes Brotli 1.2.1, and rebuild any executables compiled with Bun, since each one carries its own copy - Patch the Planet. The release itself is dated October 22 - Bun 1.4.4 release notes.
Then restart everything. A long-running process keeps the binary it started with, so a server whose package manager installed 24.22.1 at noon is still running 24.21.0 until the process restarts. Qualys put it bluntly in its guidance: restart every service that runs on Node.js - Qualys. That includes background workers, queue consumers, scheduled jobs and the process manager itself. Why this section matters: it is the actual fix, and every environment has its own way to look updated without being updated. How to apply it: work down the inventory from step 1, update each line with the method that installed it, restart, and write the observed version next to it. With the runtime handled, the same week deserves one more upgrade.
7. Step 3: Patch Next.js the Same Week
The Next.js release of October 14 is a separate set of bugs from Node2Shell, but it lands on the same servers, and if you are rebuilding images and restarting processes for the runtime anyway, shipping both together costs almost nothing extra. The release fixed three vulnerabilities, all rooted in upstream dependencies - Next.js blog. Two of them are critical, unauthenticated remote code execution scored 9.5 under CVSS 4.0, and one is a high-severity denial of service. None of them is fixed by updating Node.js, and Node2Shell is not fixed by updating Next.js, which is exactly why teams that only did one of the two this month are still exposed.
The three advisories are worth knowing by name, because scanners do not all see them. The first is GHSA-673q-mjm6-r5xr: in App Router apps using Cache Components, a use cache function that returns multiple typed arrays can lead to remote code execution, on Next.js 16.0.0 through 16.4.0, with no known workaround - GitHub advisory. The root cause sits in React's Server Components packages, and React published its own advisory for react-server-dom-webpack, -turbopack and -parcel, fixed in 19.0.9, 19.1.10, 19.2.9 and 19.3.1 - React advisory. The second is CVE-2026-96889: a memory bug in an image library used by the Image Optimization API can lead to code execution when Next.js runs on glibc-based Linux and processes SVG files, affecting every version from 10.0.0 up to 15.5.27 and 16.0.0 to 16.4.0 - GitHub advisory. The third is GHSA-p3m5-59mp-jv5r, scored 8.2: crafted requests to an App Router app with at least one Server Action can pin the CPU and block every other request in that process - GitHub advisory.
The upgrade, and the lockfile trap
The upgrade command is one line per release line. On 16.x, any version from 16.4.1 up carries the fixes; 16.4.3, published October 23, is the current one and adds only bug fixes on top. On 15.x, the fix is 15.5.28, the release npm's backport tag points to for that line.
npm install next@16.4.1 # or newer on 16.x (16.4.3 is current)
npm install next@15.5.28 # on 15.x
On the 15.x line there is a trap that this command does not cover. The image RCE is fixed by sharp 0.35.5, the image library Next.js uses, whose own advisory marks every version below 0.35.5 as affected - sharp advisory. The Next.js advisory says upgrading Next.js "ensures package managers automatically provide a fixed version", which holds for a fresh install. But next@15.5.28 declares its optional sharp dependency as ^0.34.3 || ^0.35.5 in the npm registry, so an existing lockfile that already holds sharp 0.34.5 still satisfies the range, and npm keeps it. On 16.4.1 the range is ^0.35.5, which forces the upgrade, so the trap is specific to the 15.x line. The fix is to raise sharp by name and then prove it:
npm install next@15.5.28 sharp@^0.35.5
npm ls sharp next react-server-dom-webpack
# every sharp line must read 0.35.5 or newer; any 0.34.x means the image RCE is still open
The Cloud Security Alliance's research note on this release gives the same advice from the other direction: after the build, confirm the resolved versions with npm ls sharp and the lockfile "rather than assuming the upgrade propagated" - CSA research note. The same note flags a second blind spot. The use cache and Server Actions advisories had no CVE identifiers at publication, so tools that only watch CVE feeds may never alert on them. If your dependency scanner was quiet this month, that is not evidence you are patched.
Why Next.js keeps shipping image fixes
It helps to see the October release in context. Since late August, Next.js has published four critical remote code execution advisories, three of them in image handling: an AVIF decoding flaw on August 25, an RCE in next/og image generation on September 22, and this month's SVG and use cache pair, with a September 30 batch of seven lower-severity fixes in between - Next.js security advisories. A team that patched in September to 15.5.27 or 16.3.8 is still exposed to everything in the October release.
The pattern has a structural cause. Image optimization means running native C libraries (libvips, librsvg, libheif) on files that come from the internet, which is precisely the kind of code that memory bugs live in and that AI-assisted fuzzing is now very good at finding. Hosting changes the exposure but not the duty. Netlify, for example, says the next/image path of the SVG bug is not exercised on its platform because its own image CDN serves those requests, while apps rendering untrusted input through next/og are still affected, with impact limited there to crashed function invocations - Netlify changelog. Netlify also warns that public deploy previews and branch deploys may stay vulnerable until they are deleted, which is true of preview deployments on any host: an old preview URL is an old build.
Two more same-week updates belong in this step. If your app runs on Express, take body-parser 2.4.0 (or 1.20.9 on the 1.x line), which fixes a crash any client could trigger with a compressed body followed by trailing bytes - body-parser 2.4.0 release. Express 5.3.0 declares ^2.3.0, so the same lockfile caution applies: a fresh install picks up 2.4.0, an existing lockfile may not. And if you use React Server Components packages directly, outside Next.js, update them to the React advisory's versions yourself.
Why this matters: the October Next.js bugs are just as serious as Node2Shell, they are fixed by a completely different action, and one of them can stay open after an upgrade that looks successful. How to apply it: upgrade next, raise sharp and body-parser by name, run npm ls and read the versions, then rebuild and redeploy in the same change as the Node.js update. If you work with an AI coder, tell it to run that npm ls check and paste the output, rather than accepting "upgraded" as a summary; our guide to one AGENTS.md for Claude Code, Codex and Cursor shows how to make a rule like that stick across sessions.
8. Node.js 20 and Other End-of-Life Lines: Your Real Options
For a large share of the world's Node.js, "patch" is not an available verb. Node.js 20 reached end of life on April 30, 2026, and its last release, 20.20.2, shipped on March 24 - Node.js release schedule. The Node.js project's June 2026 security release was already the first to ship fixes only for 22, 24 and 26 - Node.js June 2026 security release. The odd-numbered lines 23 and 25 are also end of life, and Node.js 18 has been unsupported since April 2025. The October advisory itself does not mince words about any of them: end-of-life versions "are always affected when a security release occurs" - Node.js security release.
The official schedule, published by the Node.js release team, shows where each supported line stands right now. Node.js 22 and 24 are in maintenance, Node.js 26 becomes the active LTS line on October 28, and the Node.js 27 alpha starts the same day under the project's new once-a-year release model.
Reading the chart as a decision gives a short table. The dates come from the release team's schedule file, and the fixed versions from this month's advisory.
| Line | Status on October 24, 2026 | Node2Shell fix | Security support ends |
|---|---|---|---|
| Node.js 26 | Current; LTS from Oct 28 | 26.12.1 | April 30, 2029 |
| Node.js 24 | LTS (maintenance since Oct 20) | 24.22.1 | April 30, 2028 |
| Node.js 22 | LTS (maintenance) | 22.23.4 | April 30, 2027 |
| Node.js 25 and 23 | End of life | None | Ended |
| Node.js 20 | End of life since April 30, 2026 | None | Ended |
| Node.js 18 and older | End of life | None | Ended |
Option 1: upgrade to 24, plan for 26
For most teams the right target today is Node.js 24. It is the most downloaded line, the default on Vercel, supported until April 2028, and it already has the fix. Node.js 26 has the longest runway, to April 2029, but it only becomes LTS on October 28, and hosting support lags: Vercel's documentation lists 24.x and 22.x as its available versions - Vercel Node.js versions. A sensible plan is to move to 24 this week and schedule 26 for early 2027, once your platforms offer it.
The jump from 20 to 24 is two major versions, so it deserves a test pass rather than a blind bump. The usual sources of friction are native addons that must be recompiled for the new runtime, a newer bundled npm (24.22.1 ships npm 11.19.1 where Node.js 20 shipped npm 10.8.2), and a newer OpenSSL (24.22.1 carries OpenSSL 3.5.9 - Node.js 24.22.1 release), which can surface in old TLS settings. On the Next.js side, 16.x requires Node.js 20.9 or newer, so the framework will not hold you back. Run the full test suite on 24, deploy to a preview, and watch the error rate for a day.
Option 2: what happens if you do nothing on a platform
Managed platforms enforce end of life differently from how you might expect. Vercel disabled Node.js 20 in project settings on October 1, 2026, but its changelog is explicit that "existing deployments with Serverless Functions will not be affected", and that only new deployments fail - Vercel changelog. The practical consequence is uncomfortable: an app that was deployed on Node.js 20 before October 1 and never redeployed is still serving traffic on a runtime that will never receive the Node2Shell fix. If you have any project you have not touched since summer, open it, check its Node.js setting, and redeploy on 22.x or 24.x. Homebrew applies the same logic on developer machines, with node@20 deprecated and due to be disabled on October 28 - Homebrew node@20 formula.
Option 3: paid extended support
If an upgrade really cannot happen soon, commercial extended support is the remaining legitimate path. HeroDevs, which sells "Never-Ending Support" builds for end-of-life Node.js, said on October 23 that remediation for 20.x was in progress, followed by 18.x and other end-of-life lines, without a release date or public pricing - HeroDevs. That can be the right call for a regulated system with a long change process. It is the wrong call for a small product team, because it turns every future runtime advisory into a purchase, and the release cadence in section 4 says those advisories are coming more often, not less.
Why this section matters: the end-of-life population is bigger than the unpatched supported population, and for that half of the world there is no quick fix, only a migration that should have started in April. How to apply it: list every app on 20 or older, upgrade the ones you can to 24 this week, redeploy anything sitting on a platform's Node.js 20 setting, and put a firm date on the rest. The next section covers what protects those apps in the meantime.
9. If You Cannot Patch Today: Stopgaps and Their Limits
Stopgaps exist for the hours or days between learning about a flaw and running the fix, and they are worth applying immediately. They are also where teams most often fool themselves, because a stopgap that covers one path feels like a fix for all of them. The honest rule for Node2Shell is simple: edge controls can only see inbound traffic, so they can help with request bodies and can never help with responses your own code fetches. Patch the Planet's advisory says it directly: request-size limits and WAF rules alone "do not cover every decoding path" - Patch the Planet.
With that rule in mind, the stopgaps fall into three groups: firewall rules you get for free, application guards you write in a few lines, and feature switches that remove a decoding path entirely. Each one below says which path it covers, so you can match it to the exposure check from section 3.
Firewall rules
On Vercel you already have one. Its WAF rules went live the same day as the Node.js release, apply automatically to every project at no cost, work regardless of the backend runtime, and show blocked requests in each project's Firewall dashboard - Vercel security bulletin. Imperva added dedicated rules validated against the exploitation attempts its own team reproduced - Imperva Threat Research. Both cover the inbound path only. Vercel also advises restricting direct access to any external Node.js backend so traffic can only reach it through Vercel, because a firewall in front of one hostname does nothing for an origin server reachable on another.
Refuse Brotli request bodies before parsing
The most effective inbound stopgap is to refuse Content-Encoding: br on request bodies before any parser touches them, while keeping gzip and deflate working. Very few legitimate clients send Brotli-compressed request bodies, so the break risk is low. Patch the Planet publishes an Express guard that does exactly this and also rejects unknown or stacked encodings such as gzip, br:
// Register before every body parser or decompression middleware.
// Source: Patch the Planet, Brotli advisory (October 23, 2026)
app.use((req, res, next) => {
const encoding = (req.get('Content-Encoding') || 'identity')
.trim().toLowerCase();
if (!['identity', 'gzip', 'deflate'].includes(encoding)) {
return res.status(415).send(
'Use gzip, deflate, or an uncompressed request body.'
);
}
next();
});
app.use(express.json());
If you cannot place a guard ahead of every parser, the fallback is to set inflate: false on each body parser, which body-parser documents as rejecting compressed bodies outright, gzip included - body-parser documentation. The same body-parser advisory from October 22 recommends that setting as its own workaround. At a reverse proxy or load balancer, the equivalent is a rule that returns 415 when the request's Content-Encoding header contains br, matched case-insensitively and including lists. Cover every route to the origin, not just the main hostname.
Guard the outbound path
The outbound path is harder, because the decoding happens inside the HTTP client the moment the response is read. Patch the Planet makes two points that save teams from false comfort. Removing br from your Accept-Encoding header does not stop a hostile server from sending Brotli anyway, and checking headers after a normal fetch() or automatic body read "can be too late" - Patch the Planet. The workable options are to use a client mode that returns raw bytes so you can refuse Brotli before decoding, to pause features that fetch arbitrary user-supplied URLs, or to move those fetches to a service already running a patched runtime. For Python services, the advisory shows a urllib3 version of this guard that reads the encoding before decoding the body, built on urllib3's documented decode_content option - urllib3 documentation.
Switch off a decoding path entirely
Some stopgaps remove a feature rather than filtering traffic. On self-hosted Next.js, images.unoptimized: true stops the built-in optimizer from fetching and decoding remote images until Node.js is patched; images are then served straight from their source URLs - Next.js documentation. For the separate October image RCE in sharp, the CSA note documents blocking SVG decoding with sharp.block({ operation: ["VipsForeignLoadSvg"] }) and turning off dangerouslyAllowSVG where it is not needed - CSA research note. In Envoy and Istio, Patch the Planet's instructions disable only the Brotli decompressor filter in both directions while keeping gzip and zstd, and warn that a disabled filter passes compressed bytes onward to whatever decodes next.
A short summary of what each stopgap actually covers helps when you pick one under pressure.
| Stopgap | Covers | Leaves open |
|---|---|---|
| Vercel or Imperva WAF | Inbound request bodies with known patterns | Everything your code fetches |
Refusing br request bodies | All inbound request bodies, including new variants | Everything your code fetches |
| Raw-mode or paused fetches | The outbound path for the clients you change | Inbound bodies, other clients |
images.unoptimized | The self-hosted Next.js image fetch path | Your other fetches and parsers |
| Envoy or Istio filter changes | The proxy's own decoder | The application behind the proxy |
Read down that table and the pattern is plain: no single stopgap covers more than one path, and the outbound path has no stopgap that is both complete and cheap. That is why every row of the scored table that relies on a stopgap lands in the bottom half. Why this matters: the attackers most likely to use Node2Shell against a small product are the ones who can make your server fetch something, through a webhook, a URL field or an AI tool, and that is exactly the path no firewall sees. How to apply it: put the inbound guard in today because it is nearly free, decide feature by feature whether any outbound fetch of user-controlled URLs can pause until the runtime lands, and treat all of it as scaffolding to remove once process.version reads a fixed release.
10. Beyond Node: Python, Bun, Envoy, Istio and the HTTP2Shell Spread
Node2Shell was never really a Node.js bug. It is a bug in Google's Brotli decoder, and Node.js is simply the most widely deployed program that bundles a copy. The CVE record covers "any Brotli libraries or binaries between 1.0.3 through 1.2.0", which in practice means every language binding, proxy and tool that decodes Brotli with that C code - CVE record. According to Aikido, OpenAI reported the bug as unauthenticated remote code execution that works in both directions, against clients that fetch and servers that receive - Aikido Security on the Python spread. That is why Aikido renamed the story HTTP2Shell on October 23: once the same flaw showed up in Python's HTTP stack, the common factor was simply "software that speaks compressed HTTP".
This matters for anyone running a mixed stack, which in 2026 is most products. A common shape is a Next.js front end with a Python backend for AI work, data jobs or scraping, and both halves decode Brotli. Patching Node.js and declaring victory leaves the Python half exactly as exposed as it was. The table below collects the fixed versions across the ecosystem as of October 24, with Next.js included so the whole month's work sits in one place.
| Software | Fixed version(s) | Notes |
|---|---|---|
| Node.js | 22.23.4, 24.22.1, 26.12.1 | No fix for 20, 23, 25 or older |
| Google Brotli (C library) | 1.0.10, 1.1.1, 1.2.1 | Distros may backport without a version change |
Python Brotli | 1.2.1 | Released October 22 |
Python brotlicffi | 1.2.1.0 | Released October 22 |
| Bun | 1.4.4 | Rebuild compiled executables |
| Envoy Proxy | 1.40.0, 1.39.3, 1.38.6, 1.37.8 | 1.36 is unsupported |
| Istio | 1.29.9, 1.30.6, 1.31.2 | 1.29 is end of life |
| Next.js (separate bugs) | 15.5.28, 16.4.1 | Plus sharp 0.35.5 |
| body-parser (separate bug) | 2.4.0, 1.20.9 | Compressed-body crash |
The Node.js, Bun, Envoy, Istio and upstream Brotli rows come from Patch the Planet's fixed-release table, reviewed on October 23 - Patch the Planet. The Brotli 1.2.1 release itself is on Google's repository - google/brotli release. Istio's 1.31.2 release is listed on its own release page - Istio 1.31.2.
Python: the libraries that decode Brotli for you
Python's exposure follows the same logic as Node's outbound path, and it is just as easy to miss. According to Aikido's analysis, urllib3 and requests decode Brotli automatically whenever the Brotli or brotlicffi package is installed, httpx does so when installed with its Brotli extra, aiohttp does when Brotli is present, and Scrapy can pull Brotli in as a dependency on its own - Aikido Security. So a Python service that never mentions Brotli in its own code can still decode attacker data, simply because a transitive dependency installed the package. Aikido also noted that advisory databases had not yet mapped the CVE to the PyPI packages on October 23, which means pip-audit and similar scanners may stay quiet for a while.
# Per environment and per container image
pip show Brotli brotlicffi
docker run --rm your-registry/your-python-app:prod pip show Brotli brotlicffi
# What pulled Brotli in, and the fix
pipdeptree -r -p Brotli
pip install -U "Brotli>=1.2.1" "brotlicffi>=1.2.1.0"
If your Python backend runs on Vercel or another function host, the package versions are yours to raise, because they ship in your deployment rather than in the platform runtime. Our guide to running a Python backend on Vercel covers how those dependencies are installed and pinned there.
Proxies, tools and bundled copies
In Envoy and Istio, exposure depends on configuration: only deployments with a Brotli decompressor filter decode untrusted data, and the fixed releases or the filter change described in section 9 close it. curl and libcurl decode Brotli through whatever library they were built against, so a statically linked build needs a new build rather than a system update. Electron apps can carry two Brotli copies, one in Chromium and one in their embedded Node.js, and a browser update does not prove the Node.js side is fixed. Linux distributions often ship the fix as a library update: Debian, for example, fixed trixie through DSA-6582-1 and bookworm through DLA-4855-1, both on the brotli source package - Debian security tracker.
Why this section matters: the CVE follows the library, not the language, so the inventory from step 1 should grow one more column for "decodes Brotli through what". How to apply it: for each Python service, check the two PyPI packages and raise them; for each proxy, check whether a Brotli decompressor filter exists; for each desktop or command line tool you ship, rebuild it on patched dependencies. If you also run PHP somewhere, the same week brought its own emergency, which our PHP2Shell guide covers step by step.
11. After the Patch: Verify, Restart, Look Back
A patch is finished when the running process proves it, not when a command succeeds. This sounds pedantic until you have watched a deployment report success while the old container kept serving traffic, or a package manager install 24.22.1 next to a process manager still holding 24.21.0 in memory. Verification has three parts: confirm the version in the running process, confirm nothing old is still running alongside it, and then look backwards at whether anything happened before you patched.
The look-back part is where most teams stop, partly because it feels like admitting a problem. It is better seen as cheap insurance. Public research showing code execution appeared on October 22, so the window worth reviewing for most apps runs from October 21 until the moment your fixed runtime went live.
Prove the fix is running
Verification should use the same evidence a skeptical colleague would ask for: a version string printed by the process itself, at a time after your deploy. Anything else, such as a merged pull request, a green pipeline or a package manager's success message, proves that a change was made somewhere, not that it is serving traffic.
Start with the startup log line from step 1. Every service should now print a fixed version when it boots, and a quick search of your logs for the old version strings after the deploy time tells you whether any instance escaped the restart. For containers, compare the image digest that is actually running with the digest you built, because a tag can be re-pointed and an orchestrator can keep old replicas alive. On Vercel and similar hosts, deploy a new build after the platform moves and confirm the number in the logs; an existing deployment keeps the runtime it started with.
# Kubernetes: which image digests are actually running
kubectl get pods -o jsonpath='{range .items [*]}{.metadata.name}{"\t"}{.status.containerStatuses [*].imageID}{"\n"}{end}'
# Any host: list startup lines that are not this week's fixed releases (or newer)
grep "\ [runtime\] node" app.log | grep -vE "node v(22\.23\.4|24\.22\.1|26\.12\.1)" | tail
Look back at the window
The signals worth checking are specific to how Node2Shell is reached. On the inbound side, look for requests carrying Content-Encoding: br on endpoints that never normally receive compressed bodies, and review the blocked requests in your WAF dashboard, which on Vercel sits in each project's Firewall view. On the outbound side, look for fetches to unfamiliar hosts through features that accept user URLs. In process logs, look for unexpected crashes and restarts with zlib or memory errors. Imperva cautions that crashes and decoder errors on their own are "not a reliable stand-alone sign of exploitation", so treat them as reasons to look closer rather than as proof - Imperva Threat Research. One useful detail for the future: patched releases expose a new decoder error code, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, for exactly the malformed input this bug relied on - Node.js 24.22.1 release. Logging Brotli decoding failures from now on turns attempts against your patched app into a visible signal.
If something does look wrong, assume the process's secrets were readable. Code running inside a Node.js process can read its environment, which usually holds database URLs, payment keys and API tokens. Rotate those first, then review what each credential could reach in the window. The less each credential can do, the smaller that review becomes, which is the argument in our guide to giving AI agents an identity instead of an API key: scoped, short-lived credentials turn "rotate everything" into "rotate one thing".
Finally, keep watching the status of the CVE itself. As of the October 23 edition of CISA's Known Exploited Vulnerabilities catalog, Node2Shell was not listed - CISA KEV catalog. A listing would mean confirmed exploitation in the wild, and it would raise the priority of the look-back for anyone who patched late. Why this section matters: an unverified patch is a hope, and an unreviewed window is an unknown. How to apply it: make the startup version log permanent, add Brotli decoding errors to your alerts, and run the look-back once, this week, while the logs still exist.
12. The Bigger Lesson: Patch Speed Is the Security Model Now
Step back from the version numbers and ask a structural question: what actually determines whether a bug like Node2Shell hurts you? Not whether the bug exists, because every large codebase contains bugs like it. What matters is the window between a fix being available and that fix running in your process, set against how quickly attackers can turn a published fix into a working attack. Both sides of that comparison are moving, and they are moving in opposite directions for teams that do nothing.
On the discovery side, the cost of finding serious bugs is collapsing. Node2Shell itself was found by an AI security product, and the program that disclosed it reports that Trail of Bits engineers working with OpenAI's models "identified hundreds of security issues" across 19 open source projects in its first sprint - OpenAI. The Cloud Security Alliance's note on the Next.js release makes the same point in its own words, describing "machine-speed vulnerability discovery" outpacing the ability of maintainers and downstream users to patch - CSA research note. The numbers in section 4 are what that looks like from the outside: 8 Node.js vulnerabilities fixed across the security releases of 2025, 50 across the first five of 2026, and four critical Next.js remote code execution advisories in seven weeks.
On the attack side, the same falling cost applies. Semgrep's write-up cites Mandiant data showing the average time to exploitation falling from 63 days in 2018 and 2019 to 32 days in 2021 and 2022, and Rapid7 data putting recent high-severity cases at roughly three days - Semgrep. A model that can find a bug from source code can also read a published patch diff. So the defender's only lever that is fully under their own control is their own patch latency: how many hours it takes to get a fixed runtime and fixed dependencies running everywhere.
Who actually owns each layer
The reason patch latency is hard is that "your app" is three layers with three different owners, and Node2Shell plus the Next.js release touched all three in one month. The dependency layer (next, sharp, body-parser) is yours, written into your lockfile. The runtime layer (the node binary) is yours on servers and containers, but your host's on serverless platforms. The platform layer (the firewall, the image service, the managed runtime) is entirely your host's. Each layer has its own update mechanism and its own lag, and a fix in one layer does nothing for another.
This week showed the lag in each layer. On the dependency layer, a lockfile, which exists to make builds reproducible, quietly preserved a vulnerable sharp after a "successful" Next.js upgrade. On the runtime layer, a managed platform whose documentation describes automatic security patching was still reporting a September runtime three days after the fix, and had not even moved to the newest non-security release of the line. On the platform layer, the free firewall rules arrived the same day, which is the platform layer doing its job well, but they cannot cover the outbound path. The lesson is not that any of these owners is careless. It is that "managed" is not the same as "current", and that the only way to know which one you have is to make the running version observable, as in step 1, and to check it.
The dependency layer has a structural fix that is worth naming, because it generalizes beyond this month. Pinning versions freezes vulnerabilities along with everything else. What works better is a floor: a minimum version per package, set from the advisory feed, that every build is raised to and checked against, with npm overrides holding transitive copies so a framework's loose range cannot keep an old library in the tree. Floors keep the reproducibility of a lockfile while refusing to reproduce a known hole.
That is the change Founden shipped this week. Founden, which builds and runs companies as autonomous software on Next.js, added a security floor on October 23: one list of minimum package versions, rebuilt from npm's advisory data rather than typed by hand, which each company's own manifest and lockfile are raised to whenever its AI builder starts work. A separate rollout carries the floor to every live company whether or not its builder is working on it, test-building each one as a preview deployment before publishing, most-visited sites first, and new companies start from a template that already holds the floor. Its entries include next 15.5.28 and 16.4.1, with sharp 0.35.5 listed as an entry of its own, for the lockfile reason described in section 7.
What changes when AI writes and runs the code
AI coders cut both ways here. On one side, they tend to reproduce the patterns that were most common in the code they learned from, and for the last few years a typical Dockerfile or CI config pinned Node.js 20, because it was the safe LTS choice at the time. The more than 103.7 million Node.js 20 downloads in July, three months after its end of life, are what those pinned configs look like in aggregate. An AI coder asked to "add a Dockerfile" can easily hand you FROM node:20 today without any warning that the line is closed. That is fixable with standing instructions: tell your AI coder, in the project's instructions file, which Node.js line and minimum versions to use, and ask it to prove upgrades with npm ls output rather than prose.
On the other side, AI agents are exactly the right tool for the tedious part of patching: finding every Dockerfile, raising every lockfile, running every test suite and opening every preview. Running those agents unattended is now practical, as our guide to running Claude Code unattended describes, and splitting the work across several agents at once is covered in our parallel agents playbook. The products that will absorb this new release cadence best are the ones where patching is a routine, automated job with tests and previews in front of it, rather than an annual migration project. The same reasoning is why our guide on when to graduate from a vibe-coding tool asks who owns the runtime under your app before it asks anything about features.
Pressure-testing the argument
It would be easy to overstate this. Not every advisory is an emergency: Node2Shell's own CVSS vector admits an attack requirement, the public exploit was unreliable, and as of October 23 nobody had reported it being used in the wild. Patching fast also carries its own risk, because every update is a change, and change is where outages come from. The October 14 Node.js feature releases are a small reminder that new is not automatically safe. And a flood of AI-found bugs can be noise as well as signal; OpenAI itself stresses that every finding in its program is reviewed by a human before it reaches a maintainer, precisely because models produce many false positives - OpenAI.
None of that weakens the conclusion; it shapes it. The goal is not to patch recklessly but to make patching cheap: small, frequent updates, each one tested and previewed, so that a security release is a routine change that ships in a day rather than a risky one that waits a month. Why this matters: the release cadence of 2026 makes the annual upgrade plan obsolete. How to apply it: measure your own patch latency on this release, from October 21 to the moment your last service reported a fixed version, and treat that number as the security metric to improve. Our guide to the AI-native company tech stack covers which layers to own and which to rent with that metric in mind.
13. Conclusion: A Decision Framework for This Week
Node2Shell is a serious bug with a simple fix, and the October Next.js release is a serious set of bugs with a simple fix that has one trap in it. Almost all of the difficulty this week comes from finding every place the fix has to go and proving it arrived. The framework below orders the work by what you are running, so you can start at your own line and stop when the checklist is done.
If you are on Node.js 22, 24 or 26 anywhere you control, patch in place today: 22.23.4, 24.22.1 or 26.12.1, rebuilt images, restarted processes, a logged version. If you are on Node.js 20 or older, or on 23 or 25, there is no patch for you; move to 24 this week, redeploy anything left on a platform's Node.js 20 setting, and only buy extended support if a migration truly cannot happen. If you are on a managed platform, keep its firewall rules, add the startup version log, and redeploy as soon as the version moves; until then, protect outbound fetches of user-controlled URLs yourself.
Whatever your runtime, the same short checklist closes the month:
| Area | Done when |
|---|---|
| Runtime | Every copy runs 22.23.4, 24.22.1, 26.12.1 or newer, proven by process.version in logs |
| Next.js | 15.5.28, or 16.4.1 and newer, with npm ls sharp showing 0.35.5 or newer |
| Express | body-parser 2.4.0 or 1.20.9, plus the Brotli request guard until the runtime lands |
| Python and proxies | Brotli 1.2.1, brotlicffi 1.2.1.0, and fixed Envoy or Istio where Brotli filters run |
| Look-back | One review of the October 21 window, and secrets rotated if anything looks wrong |
The order of that list follows the scored table at the top: the runtime first because it is the only thing that closes both paths, the framework second because it carries its own critical bugs, and the stopgaps last because they only buy time. A team that works through it this week ends October in a better position than it started, because the work leaves behind a version inventory, a startup log line and a habit of checking npm ls that will make the next advisory a one-day job. If you are building a new product rather than patching an old one, our guides to building a live app with Claude Code and what it costs to build an app with AI cover how to start on a current runtime and keep it there.
This guide reflects the Node.js, Next.js and Brotli advisories as of October 24, 2026. Fixed versions, platform rollouts and exploitation status change quickly after a disclosure like this, so check the official Node.js security post, the Next.js advisories and your host's status page before you rely on any version number here.