Node2Shell (CVE-2026-111497): a practical patch runbook for Node.js teams
Find every Node.js runtime you run, patch it and prove it's patched
On 21 October 2026 Node.js shipped security releases for the 22.x, 24.x and 26.x lines. The headline fix is CVE-2026-111497, a flaw in the Brotli decoder bundled in node:zlib, now widely called Node2Shell. According to the Node.js advisory, malformed Brotli data could make the streaming decoder write past the end of a small internal buffer. Node.js rates it High, and Google, which assigned the CVE, scores it 9.2 on CVSS v4. The possible outcomes range from a crashed process to memory disclosure and, in some cases, code execution.
The fix is simple: run a patched Node.js. The hard part is finding every place Node.js runs. This post is the runbook we use.
Fixed versions
| Line | Patched in |
|---|---|
| Node.js 22 (LTS) | 22.23.4 |
| Node.js 24 (LTS) | 24.22.1 |
| Node.js 26 (Current) | 26.12.1 |
| 18, 20, 23, 25 and other EOL lines | No fix. Move to a supported line. |
The same release also fixes two TLS hostname-verification bypasses rated High, so it's worth taking even if you never touch Brotli.
Updating an npm package doesn't fix this. The vulnerable code ships inside the Node.js binary. The npm package called brotli is a separate thing.
Step 1: work out how exposed you are
The advisory says the risk is to apps that decompress attacker-controlled Brotli data with node:zlib. Grep for the obvious entry points:
# Direct use of the Brotli decoder
grep -rnE "createBrotliDecompress|brotliDecompress(Sync)?" --include=*.{js,ts,mjs,cjs} src/
# Middleware or clients that inflate request/response bodies
grep -rnE "Content-Encoding|decompress|inflate" --include=*.{js,ts} src/ | grep -i br
Then think about indirect paths:
- Inbound: body parsers or proxies that decode
Content-Encoding: bruploads before your handler runs. - Outbound: HTTP clients that fetch user-supplied URLs (link previews, webhooks, scrapers, image proxies) and decompress responses automatically.
- Data pipelines: queues, partner feeds or uploads carrying Brotli-compressed payloads.
A site that only sends Brotli responses is much less exposed, but it should still be patched.
Step 2: inventory every runtime
This is where teams usually miss something. Check each of these:
# On every server and CI runner
node -v
# Container images
grep -rn "^FROM node" --include=Dockerfile* .
# Version pins
cat .nvmrc .node-version 2>/dev/null
jq '.engines' package.json
# CI configuration
grep -rnE "node-version|setup-node" .github/ .gitlab-ci.yml 2>/dev/null
Don't forget hosting dashboards: Node.js selectors on cPanel/CloudLinux hosts, the runtime setting on serverless functions, and platform-as-a-service build images.
Step 3: patch in a way that sticks
- Bump to the patched release (22.23.4, 24.22.1, 26.12.1 or newer on the same line).
- Pin the patch version in Docker images, for example
FROM node:22.23.4-bookworm-slim, instead of a floating tag on an image you never rebuild. Then let Renovate or Dependabot propose base-image bumps. - Rebuild and redeploy. A patched base image does nothing until the container is rebuilt and the process restarted.
- Update CI too. With
actions/setup-node, a range likenode-version: 22can be satisfied by an older 22.x already cached on the runner. Pin the exact patch version or setcheck-latest: true, and check self-hosted runners separately. - On managed platforms, the provider usually ships the patched runtime, but often only takes effect when you redeploy or pick the new version.
Step 4: prove it
Add a cheap check so a stale runtime can't slip back in:
// scripts/check-node.mjs: fail the build on a known-vulnerable runtime
const min = { 22: [23, 4], 24: [22, 1], 26: [12, 1] };
const [maj, mi, pa] = process.versions.node.split('.').map(Number);
const need = min[maj];
if (!need) {
console.error(`Node ${process.versions.node} is not a supported line`);
process.exit(1);
}
if (mi < need[0] || (mi === need[0] && pa < need[1])) {
console.error(`Node ${process.versions.node} is missing the Oct 2026 security fixes`);
process.exit(1);
}
Run it in CI and as a container start-up check, and log process.versions.node at boot so your observability tool can tell you what's actually running.
Step 5: look beyond Node.js
The same Brotli decoder code is used in other runtimes and libraries, which is why you'll also see the names Broccoli2Shell and HTTP2Shell. Check advisories for Bun, Python Brotli bindings and anything else in your stack that bundles Brotli. Your dependency scanner or platform security page should list what applies to you.
If you can't patch today
The Node.js advisory doesn't list a workaround. As a stopgap, stop accepting Brotli-encoded request bodies from the internet, avoid decompressing untrusted Brotli data, and patch as soon as you can.
FAQ
Are Next.js, Express or NestJS affected? The frameworks aren't the flaw. The runtime is. Any app on an unpatched Node.js should be updated, starting with apps that decode untrusted Brotli input.
We're on Node 20. Is there a backport? No. Node.js says end-of-life lines are affected when a security release happens, and they don't get fixes. Plan the move to 22 or 24 now.
Is a container rebuild really necessary if the host is patched? Yes. The container ships its own Node.js binary, so patching the host doesn't touch it.
If you run Node.js or Next.js apps for a business and want help with upgrades and maintenance, our web development team handles this for clients in the USA, UK, Canada and Australia.
Originally published on the Dzimark blog.
About the author
- I run Dzimark (https://dzimark.com), a website, mobile app and AI development company working with businesses in the USA, UK, Canada and Australia. I write about Shopify, WordPress, React/Next.js and AI chatbots for business.
Comments
Join the discussionNo comments yet. Be the first to comment.
