Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

CVE-2026-111497: Check and Patch Node.js Brotli RCE

CVE-2026-111497 is an out-of-bounds write in the Brotli decoder bundled with Node.js 22, 24, and 26. Node.js fixed it on 21 October 2026. CISA added it to the Known Exploited Vulnerabilities catalogue on 27 October with

CVE-2026-111497: Check and Patch Node.js Brotli RCE

CVE-2026-111497 is an out-of-bounds write in the Brotli decoder bundled with Node.js 22, 24, and 26. Node.js fixed it on 21 October 2026. CISA added it to the Known Exploited Vulnerabilities catalogue on 27 October with a required-action date of 10 November for US federal civilian agencies — a useful benchmark for everyone else's timeline.

The fix has been available for two weeks. What's left is an inventory problem, not a patching problem.

node --version
v24.22.2   <- patched
v24.20.0   <- vulnerable
v20.19.5   <- end of life, vulnerable at every patch level
Line Status Minimum fixed Latest (5 Nov)
26.x Current 26.12.1 26.13.0
24.x LTS 24.22.1 24.22.2
22.x LTS 22.23.4 22.23.4
25.x, 23.x, 21.x, 20.x, 18.x, 16.x EOL none migrate

Install the latest release in your line, not the bare minimum.

What the bug is

A crafted Brotli stream corrupts decoder memory, which can crash the process, leak memory, or run attacker-supplied code. The Node.js advisory describes an invalid block switch being reported as a temporary need for more input instead of a permanent format error, so the streaming decoder kept appending input bytes to an eight-byte internal buffer. The patched decoder rejects that state with a new error, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH.

The root bug is in Google's Brotli library (1.0.3 through 1.2.0), fixed upstream in 1.0.10, 1.1.1, and 1.2.1. Node.js cherry-picked the commit rather than bumping the bundled library version — which matters, because process.versions.brotli is not a patch indicator. The version string from node --version is the proof.

Two nicknames circulate: Node2Shell and HTTP2Shell. Neither is official, and HTTP/2 isn't required to trigger it. Three authorities give three severities — Node.js says High, Google's CVE record scores 9.2 on CVSS 4.0, NVD scores 9.8 on CVSS 3.1 — so quote the authority along with the number.

Where you're actually exposed

Node.js doesn't decode Brotli on its own. Something in your stack has to call the decoder, and there are three paths:

  • Request bodies via a body parser. Express 5.x depends on body-parser 2.x, which inflates gzip, br, deflate and zstd with inflate defaulting to true. Express 4.x uses body-parser 1.x — gzip and deflate only, no Brotli path. Check with npm ls express body-parser.
  • Outbound fetch(). Node's built-in fetch auto-decodes br responses, so any fetch of a URL an attacker can influence decompresses attacker-controlled data. Image optimization and webhook callbacks both land here.
  • Direct node:zlib calls — brotliDecompress, brotliDecompressSync, createBrotliDecompress on input from outside your trust boundary. Updating Express or body-parser changes nothing. The decoder is in the runtime. (body-parser 2.4.0 fixes a different CVE, 2026-111181.)

Two more hiding places: Bun 1.0.21 through 1.4.3 ships the same library, fixed in 1.4.4 — and anything built with bun build --compile keeps the old decoder until rebuilt. The same CVE also has advisories for the PyPI brotli package, the .NET decoder, and a Perl binding.

Next.js: inherits whatever Node.js runs it. Vercel patched its managed runtime for new deployments on 28 October and told Node 20 projects to move to 22.x or 24.x and redeploy. Self-hosted needs the same runtime upgrade as any other service.

Exploitation status

Confirmed. CISA holds evidence of in-the-wild exploitation. Sysdig superseded its own earlier "no exploitation" assessment after ELLIO confirmed on 26 October; Datadog corroborated on 29 October. If an internal memo cites the first 48 hours of coverage, it's stale.

It isn't trivial to exploit — a public PoC appeared on 22 October and on standard builds produces a crash and a limited memory read, and Imperva's successful code execution inside an official image took multiple attempts. A crash-only attempt is still an outage, so unreliable RCE doesn't lower the priority.

Containers are where the inventory breaks

The runtime is baked into the image, so the host's Node version tells you nothing.

docker run --rm <registry>/<image>:<tag> node --version

Three rules:

  1. Pull before you build. A floating tag like node:24 resolves to whatever your builder cached, so a rebuild can quietly reproduce the old version.
  2. Pin exact tags. FROM node:24.22.2-slim, or the digest. A tag with no version selects Current, not an LTS line.
  3. Verify the pushed artefact, not the Dockerfile. That output is the evidence for the ticket. Then every pipeline has to pull the rebuilt image and every workload has to restart on it. A registry scan finds the pre-patch images your deployment list forgot.

Don't stop at servers. CI runners (add a node --version step so every build records its runtime), developer laptops (nvm ls or fnm list — an old default in a shell profile is a common miss), and compiled Bun binaries all count.

If you can't upgrade today

Each of these needs an expiry date tied to the upgrade ticket:

  • Block Content-Encoding: br at the edge. Removes the inbound body-parser path. Vercel's verdict on its own WAF rule applies to yours — it reduces exposure, it doesn't replace patching.
  • inflate: false in body-parser rejects compressed bodies outright. That breaks clients that compress uploads, so check your API consumers.
  • Accept that outbound traffic stays exposed. A WAF never sees the response your service fetches from a third-party URL, and Node has no per-request switch to skip decoding. Restrict which URLs unpatched services may fetch.
  • Watch for SIGSEGV crashes, unexpected child processes, and outbound connections to the cloud metadata endpoint. That last one turns one vulnerable service into a credential-theft event. ## Node 20 is the hard case

It's end of life, it's affected at every patch level, and it's the line a lot of teams are still on. Options are a migration to 22.x or 24.x, or a commercial extended-support contract that backports while you migrate.

Worth separating two kinds of upgrade here. A patch-level bump inside the same major (24.20 → 24.22.2) carries little compatibility risk and should ship today, with a canary so a surprise hits one instance rather than all of them. A major migration is a different project — native addons need rebuilding, deprecated APIs surface, test suites find gaps. Still mandatory for Node 20, but it deserves a plan with temporary controls covering the interval, not a panicked Friday deploy.

Then do it again next week

Node.js has pre-announced another HIGH-severity release for 22.x, 24.x, and 26.x on or shortly after 11 November. Same sweep.

Worth making permanent while this is fresh: a CI gate that fails a build on an end-of-life or below-minimum runtime, pinned base images, and a shorter list of places where your services decompress untrusted input.

Full write-up — the version matrix, per-channel upgrade commands, exposure paths, and the full response checklist: teamvoy.com/blog/cve-2026-111497

Written by Teamvoy. More engineering writing at teamvoy.com/blog.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.