Skip to main content

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

Updated 5 min readMarkdown

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: br uploads 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

  1. Bump to the patched release (22.23.4, 24.22.1, 26.12.1 or newer on the same line).
  2. 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.
  3. Rebuild and redeploy. A patched base image does nothing until the container is rebuilt and the process restarted.
  4. Update CI too. With actions/setup-node, a range like node-version: 22 can be satisfied by an older 22.x already cached on the runner. Pin the exact patch version or set check-latest: true, and check self-hosted runners separately.
  5. 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.

No comments yet. Be the first to comment.