Security· Security Disclosure

Node.js's October 2026 Brotli Decoder RCE: CVE-2026-111497

Find every runtime exposed to CVE-2026-111497, a High-severity Node.js Brotli bug fixed in 22.23.4, 24.22.1, and 26.12.1 — and never on end-of-life lines.

Atlas Security
Security Advisories
14 min read · 3,064 words

Overview

Node.js shipped fixes on October 21, 2026 for CVE-2026-111497, an out-of-bounds write in the Brotli decompressor built into official Node.js binaries. The project rates it High, and its advisory title says the bug "permits remote code execution" (RCE). The patched builds are 22.23.4, 24.22.1, and 26.12.1; every end-of-life line stays unpatched for good.

Upgrading is the easy part. Finding every Node runtime you operate is harder: containers, version managers, CI images, distro packages, and bundled apps each carry their own copy of the decoder, and node -v on a host describes exactly one of them.

Key facts

FieldDetail
CVECVE-2026-111497
BugOut-of-bounds write in the Brotli decoder Node bundles in deps/brotli and exposes through node:zlib
SeverityHigh — the Node.js rating. No CVSS score has been published.
Impact, per Node.js"Node.js Brotli decoder out-of-bounds write permits remote code execution"
Fixed in22.23.4 (Jod LTS) · 24.22.1 (Krypton LTS) · 26.12.1 (Current)
Never fixedEvery end-of-life line: 25.x, 23.x, and ≤ 20.x
CreditReported by Matteo Collina (mcollina) and fixed by Antoine du Hamel (aduh95), per Node.js
CVE recordStill not on cve.org or NVD as of 3:45 PM CDT, October 21 — unusually slow, given that PHP's two critical CVEs from the same morning are already on both.

Who is affected

For official builds — the nodejs.org binaries and the official Docker images — the version decides it. If the major version isn't 22, 24, or 26, no fix is coming.

Release lineStatus on Oct 21, 2026Fixed inSupported until
26.xCurrent — Active LTS from Oct 2826.12.1Apr 30, 2029
24.x "Krypton"Maintenance LTS24.22.1Apr 30, 2028
22.x "Jod"Maintenance LTS22.23.4Apr 30, 2027
25.xEnd-of-life since Jun 1, 2026no fix · last release 25.9.0—
23.xEnd-of-life since Jun 1, 2025no fix · last release 23.11.1—
20.x "Iron"End-of-life since Apr 30, 2026no fix · last release 20.20.2—
18.x "Hydrogen"End-of-life since Apr 30, 2025no fix · last release 18.20.8—
16.x and olderEnd-of-lifeno fix—

Distro packages, bundled runtimes, and Bun

  • Distro packages. Debian's, Alpine's, Fedora's, and RHEL's nodejs packages are built with --shared-brotli, linking the system Brotli library instead of Node's bundled copy (Debian rules, Alpine APKBUILD, Fedora spec, CentOS Stream 10 spec for RHEL). There, the fix has to arrive through the distro's brotli package, and upstream Brotli hasn't tagged a release containing it. Neither node -v nor the probe proves the decoder is fixed on those installs; your distro's advisory does. As of 3:45 PM CDT, none of the Debian, Ubuntu, or Red Hat trackers — Red Hat's Bugzilla covers Fedora and RHEL — listed this CVE.
  • FROM node:22-alpine and apk add nodejs are different channels. The official image installs Node's own musl build and was rebuilt on October 21. apk add nodejs installs Alpine's package — 24.18.1 on Alpine's edge branch when we checked, linked against system Brotli.
  • Single executable applications and bundled runtimes. SEA binaries and apps that ship their own Node ignore the system install. The answer lives in the build: which Node version produced the binary, or process.version logged at startup. For third-party software, ask the vendor.
  • Bun. Bun isn't Node, so this CVE doesn't cover it, but Bun builds its own copy of Google's Brotli, pinned to v1.2.0, which predates the fix; its decoder source matches the unpatched one in Node 24.22.0. Bun's fetch() uses that copy to decode br responses. As of 3:45 PM CDT, the latest release was Bun 1.4.3 (October 10), Bun had published no advisory, and a community pull request proposing the fix (#46351) had no maintainer response. We haven't tested Bun's exposure.

How the vulnerability works

Brotli is a compression format, best known as the br in HTTP's Content-Encoding: br. Node.js includes Google's C implementation of it (RFC 7932) in deps/brotli and has exposed the decoder through zlib since v11.7.0 (backported to v10.16.0).

The bug is in decompression: specially crafted Brotli data can make the decoder write past the end of the memory it was given. The decoder is native code running inside your Node process, so that write corrupts the process's memory. At minimum, that typically means a crash, which is a denial of service. At worst, given a working exploit, it means code execution with the privileges of the Node.js process. That's the general consequence of this bug class, not Node's wording; the advisory says only that the bug "permits remote code execution."

To reach it, an attacker needs your application to decompress Brotli data they control. That happens more often than it sounds: Node's built-in fetch() decompresses Brotli responses automatically, and Express 5 decompresses request bodies by default. The next section maps where that happens.

Is it a Node.js bug or a Brotli bug? The flawed code is Google's Brotli decoder, and the change that fixes it landed upstream in February, in a commit that doesn't mention security. The CVE belongs to Node.js because Node ships its own copy of that decoder: you get the fix by updating Node, not a separate library. Distro packages linked against system Brotli are the exception, as above.

The fix

Node's patch is a cherry-pick of upstream commit 0ed4268, "Detect and report invalid stream earlier," authored by Evgenii Kliuchnikov at Google and dated February 27, 2026. A Brotli stream can split its literals, commands, and distances into block types and switch between them mid-stream. The commit tightens validation of those block switches in c/dec/decode.c, so the decoder rejects a malformed stream with a new error code, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (−17), in a slot the header had marked reserved.

google/brotli · 0ed4268 · Feb 27, 2026
c/dec/decode.c
58 lines added, 29 removed
c/include/brotli/decode.h
1 lines added, 3 removed
2 files changed59 lines added, 32 removed

Node landed the same patch on all three lines, plus a companion change that exposes −17 to JavaScript as zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, the constant the probe reads. Both arrived through a private security pull request, nodejs-private/node-private#1002; neither it nor the original report is public.

Node.js commits
nodejs/node · committed Oct 20, 2026
22.x67d2947
Cherry-pick of 0ed4268
59 lines added, 32 removed
24.x49dff6f
Cherry-pick of 0ed4268
59 lines added, 32 removed
26.xa6040b5
Cherry-pick of 0ed4268, plus a Nix build-file update
67 lines added, 33 removed
22.x zlib constanta18ec92
Exposes −17 to JavaScript; 27 of the 28 lines are a test
28 lines added, 0 removed

Which applications are exposed

The advisory doesn't list affected APIs or attack vectors. What follows is our reading of the code paths that hand someone else's bytes to Node's Brotli decoder — exposure, not confirmed exploit paths. A br response or body is one sent with Content-Encoding: br.

Code pathDecodesDefaultOff switch
Built-in fetch() (undici)br responsesOnNone
axios 1.xbr responsesOndecompress: false
node-fetch 3.xbr responsesOncompress: false
got 16.xbr responsesOndecompress: false
undici's interceptors.decompress()br responsesOpt-in—
Express 5 parsers (body-parser 2.x)br request bodiesOninflate: false (refuses every compressed body)
Express 4 parsers (body-parser 1.x)Nothing — br bodies get a 415——
zlib Brotli APIs (brotliDecompress, createBrotliDecompress)Whatever your code passes in——
DecompressionStream("brotli") (Node 22+)Whatever your code pipes through it——

We read each of these in source: undici as bundled in 22.23.4, axios 1.20.0, node-fetch 3.3.2, got 16.1.0, undici 8.12.3's interceptor, body-parser 2.3.0, and Node's web-streams compression. None of these libraries has a bug. They all hand the bytes to the same zlib decoder — that's the point.

Accept-Encoding doesn't protect you. Node's fetch() asks for Brotli on every HTTPS request by default (br, gzip, deflate; 26.x adds zstd), but whether it decodes depends on the response's Content-Encoding header, not on what the request asked for. A server that wants to send Brotli can. Plain http:// requests don't advertise Brotli, yet fetch() still decodes a br response — and on a plain-HTTP path, anyone in the middle can supply one.

Patch these workloads first:

  • Internet-facing APIs that accept compressed request bodies. Express 5's parsers do by default.
  • Services that fetch URLs other people choose: link previews and unfurlers, webhook delivery to customer-configured endpoints, crawlers, feed readers, image proxies, and agent tools that fetch arbitrary pages.
  • Plain-HTTP fetches, for the reason above.
  • Anything that runs zlib over uploads or third-party payloads.

How to check your systems

Checking only the host misses containers. A server can run a patched node while every container on it runs a base image from last spring. Work through each layer.

Check a Node.js binary

Ask the binary itself. The fix added a new decoder error code, and Node exposes it to JavaScript. Set the probe once; the commands below reuse it:

Shell
1PROBE="require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH || 'missing'"2node -p "$PROBE"3# -17      → this build includes the fix4# missing  → it doesn't

We ran it against the official Linux x64 builds: 22.23.4 prints -17 and 20.20.2 prints missing. The 24.22.1 and 26.12.1 sources define the same constant, and the releases before them don't. It only reads a constant — nothing gets decompressed.

Check hosts and version managers

Shell
1node -v2which -a node          # every node on PATH, in order — often more than one34nvm ls                 # nvm5fnm list               # fnm6volta list node        # Volta7asdf list nodejs       # asdf

To probe every binary on the PATH in one pass:

Shell
1which -a node | while IFS= read -r n; do2  printf '%s  %s  ' "$n" "$("$n" -v)"3  "$n" -p "$PROBE"4done

Version managers keep old runtimes installed, and a project's .nvmrc, .node-version, .tool-versions, or package.json volta field can select one per directory. The repository sweep finds those pins.

Check images and running containers

Shell
1# Local images2docker images --format '{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}'34# Inside an image (--entrypoint overrides the image's own ENTRYPOINT)5docker run --rm --entrypoint node <image> -v6docker run --rm --entrypoint node <image> -p "$PROBE"78# Running containers9docker ps --format '{{.ID}}\t{{.Image}}\t{{.Names}}'10docker exec <container> node -v11docker exec <container> node -p "$PROBE"

Distroless Node images have no shell, and their entrypoint is already node, so pass the flags straight through: docker run --rm <image> -v.

The official node Docker images were rebuilt on October 21. Docker's 22, 24, 26, and lts tags were re-pushed between 9:38 and 9:45 AM CDT; rebuild with docker build --pull so the base image is fetched fresh rather than reused from cache. Two things won't move on their own. Images pinned by digest (node:22@sha256:…) stay put until someone updates the digest. And end-of-life tags are frozen: node:20 still pulls, was last pushed April 22, 2026, and will run the last 20.x build indefinitely.

Check Kubernetes

Shell
1# Every pod and its container images, cluster-wide2kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{range .spec.containers[*]}{.image}{" "}{end}{"\n"}{end}'34# The actual build inside one container5kubectl exec -n <namespace> <pod> -c <container> -- node -v6kubectl exec -n <namespace> <pod> -c <container> -- node -p "$PROBE"

A tag is a first pass, not an answer. node:20-alpine is obviously out, but node:lts, node:22, and every private tag say nothing about the build inside until you exec into it.

Check repositories and CI pipelines

Shell
1# Base images in Dockerfiles and Containerfiles — any registry, any variant2grep -rniE --exclude-dir=node_modules \3  --include='Dockerfile*' --include='Containerfile*' --include='*.dockerfile' \4  '^[[:space:]]*FROM[[:space:]].*node' .56# CI pipelines and compose files7grep -rnE --exclude-dir=node_modules --include='*.yml' --include='*.yaml' \8  'node-version|node:[0-9]|image:.*node' .910# Version-manager pins: .nvmrc, .node-version, asdf11grep -rH --exclude-dir=node_modules --include='.nvmrc' --include='.node-version' '' .12grep -rH --exclude-dir=node_modules --include='.tool-versions' '^nodejs' .1314# package.json engines and Volta pins15grep -rnE --exclude-dir=node_modules --include='package.json' '"node"[[:space:]]*:' .

We tested these with GNU grep 3.8 against a fixture repo: they catch --platform and registry-prefixed FROM lines, distroless bases, and pins under .github/, and they skip node_modules.

How to fix

Upgrade to Node.js 24 or 26

If you're on 22.x, 24.x, or 26.x, take the patch release. If you're coming from 20 or older, we recommend 24, an LTS line supported until April 30, 2028. 26 becomes LTS on October 28 and is supported until April 30, 2029: the longer runway, if your team can absorb more breaking changes in one jump. Landing on 22 means repeating this migration next spring.

Shell
1# nvm2nvm install 24 && nvm alias default 2434# fnm5fnm install 24 && fnm default 2467# Volta8volta install node@24910# Containers: bump the base image (e.g. FROM node:24-bookworm-slim), then11docker build --pull -t <image> .

On distro-packaged Node, watch your distro's advisories for both nodejs and brotli. If a host needs the fix before the distro ships one, move it to the official nodejs.org binaries or the official Docker images.

If you can't leave an end-of-life line yet

Node.js's end-of-life page lists commercial support through the OpenJS Foundation's Ecosystem Sustainability Program: security patches for EOL versions from HeroDevs (Never-Ending Support) and TuxCare (Endless Lifecycle Support), and an upgrade program from NodeSource. We haven't evaluated them. Ask whether a vendor's builds cover CVE-2026-111497 before you count on it, and keep Node's framing in mind: commercial support "should be viewed as a temporary solution."

Reduce exposure until you can patch

Node.js publishes no workaround. The steps below are Atlas's interim recommendations, and none of them is a fix.

Refuse Brotli request bodies at the edge. Check your access logs first: if no legitimate client sends Brotli-encoded bodies, this costs you nothing.

NGINX
1# Interim: refuse Brotli-encoded request bodies until every upstream runtime is patched.2# Matches the br token in the Content-Encoding list, not substrings like "brotli".3if ($http_content_encoding ~* "(^|,)[[:blank:]]*br[[:blank:]]*(,|$)") {4    return 415;5}

Or do it in the app, registered before any body parser:

JavaScript
1// Interim: refuse Brotli-encoded request bodies until the runtime is patched2app.use((req, res, next) => {3  const codings = (req.headers["content-encoding"] ?? "").toLowerCase().split(",");4  if (codings.some((c) => c.trim() === "br")) return res.status(415).end();5  next();6});

Stop decoding responses you don't trust. For calls to endpoints you don't control, turn decompression off in clients that allow it and ask for uncompressed responses:

JavaScript
1// axios 1.x2await axios.get(url, { decompress: false, headers: { "Accept-Encoding": "identity" } });34// node-fetch 3.x (imported as fetch from "node-fetch")5await fetch(url, { compress: false, headers: { "Accept-Encoding": "identity" } });67// got 16.x8await got(url, { decompress: false, headers: { "accept-encoding": "identity" } });910// undici's request() never decodes; a br body arrives as raw bytes (import { request } from "undici")11const { statusCode, headers, body } = await request(url, { headers: { "accept-encoding": "identity" } });

Built-in fetch() has no such switch. The simplest swap is request() from the undici package — the library Node's fetch() is built on — which never decompresses anything, so the decoder can't run unless you pipe a br body through zlib yourself. Running the worker that makes untrusted calls as a separate, low-privilege process is still worth it as defense in depth: a crash or a compromise stays contained.

Patch the riskiest processes first — the workloads listed under exposed applications.

End-of-life versions

Node.js is blunt about what end-of-life means. From the advisory:

"It's important to note that End-of-Life versions are always affected when a security release occurs. To ensure your system's security, please use an up-to-date version as outlined in our Release Schedule."

And from its end-of-life page:

"When new security releases reveal issues and patches in newer major lines, even if the same vulnerability affects EOL release lines, there will not be any new releases for them. Users still clinging on to EOL release lines and using affected code paths will be immediately vulnerable to attacks exploiting these disclosed vulnerabilities."

Node 20 has been end-of-life for 174 days. Its final release, 20.20.2, doesn't have the change: the new error code is absent from its bundled Brotli headers, as it is in 18.20.8, 23.11.1, and 25.9.0. That shows the fix is missing; Node's policy is what says those versions are affected.

Node 22 is patched, but as of this release it has 191 days of support left. Teams moving off 20 shouldn't stop there.

Timeline

Dates come from Brotli's releases, the Node.js release archive, and Docker Hub. Times are CDT (UTC−5).

WhenWhat
Oct 27, 2025Brotli v1.2.0 — still the latest tagged release
Feb 27, 2026The fix lands in google/brotli. The commit message doesn't mention security.
Mar 24, 2026Node 20.20.2, the last Node 20 release, ships without it
Apr 30, 2026Node 20 reaches end-of-life
Oct 14, 2026Node.js pre-announces the October security release
Oct 21 · ~5:30 AM22.23.4, 24.22.1, and 26.12.1 binaries published
Oct 21 · 7:08 AMAdvisory updated with CVE details
Oct 21 · 9:38–9:45 AMRebuilt node:22, node:24, node:26, and node:lts images on Docker Hub

The code change was public for 236 days before it shipped as a CVE — 62 of them before Node 20 reached end-of-life. That isn't a knock on either project: fixes often land before anyone has established their security impact, and Node hasn't said when the issue was reported. But it is what risk looks like in vendored C dependencies. Your exposure ends when your runtime ships the fix, not when upstream writes it.

It also means nothing built from a tagged Brotli release contains this change yet. The fix sits 41 commits past v1.2.0, and google/brotli has published no advisory. That's a statement about code, not a claim that other Brotli consumers are vulnerable — upstream hasn't said, and we won't guess.

Other vulnerabilities fixed in this release

The release fixed 10 CVEs: four High, three Medium, and three Low. Titles are as Node.js published them.

CVESeverityTitleLines
CVE-2026-111497HighNode.js Brotli decoder out-of-bounds write permits remote code execution26.x, 24.x, 22.x
CVE-2026-105482Highdns.resolve truncates names containing NUL char26.x, 24.x, 22.x
CVE-2026-105483HighTLS certificate hostname bypass when the IP is written fullwidth digits26.x, 24.x
CVE-2026-106069HighTLS certificate hostname bypass when the host loosely resolves to an IP address26.x, 24.x, 22.x
CVE-2026-105481MediumKeep-alive socket process a response that was written before the request was sent26.x, 24.x, 22.x
CVE-2026-106072MediumImproper certificate validation for non-canonical IPv4 hostnames allows DNS SANs to authenticate IP endpoints26.x, 24.x, 22.x
CVE-2026-106074Mediumcrypto.randomUUID() and randomInt() entropy caches are saved into user-land startup snapshots and SEAs, so every process repeats the same values26.x, 24.x, 22.x
CVE-2026-106070LowNode.js ZipFile returns attacker-controlled prefix bytes after failed ZIP header validation26.x
CVE-2026-106071LowPermission Model filesystem over-grant via wildcard radix lookup26.x
CVE-2026-106073LowPermission Model bypass with fs.realpathSync26.x, 24.x, 22.x

Two of the other three Highs are hostname-verification bypasses in the certificate check Node runs on the client side of a TLS handshake. The same upgrade covers them, plus dependency updates the advisory says address public vulnerabilities: c-ares 1.34.9, root certificates from NSS 3.131.1 (citing the eMudhra intermediate CA compromise), OpenSSL 3.5.9 on 24.x and 22.x, SQLite 3.53.4 on 22.x, and undici on all three lines.

Updates to this post

We'll revise this post and mark it updated when any of these change: the CVE record, or a CVSS score, on cve.org or NVD; distro advisories for nodejs or brotli; a tagged Brotli release that includes the fix; any public exploit or report of exploitation.

Get help

If you'd rather not do this alone — the runtime inventory, the upgrade plan, or a review of what's exposed — talk to our advisors.

Atlas Service
Atlas Technical Consulting
Senior advisors for security audits, code reviews, and DevOps work — starting with a two-hour discovery block, no retainer commitment, and a 24-hour response SLA.

October 21 brought a second critical runtime disclosure with the same container-inventory problem: two unauthenticated RCEs in PHP, CVSS 9.2, where mod_php is the confirmed case and end-of-life branches get no official fix.

Sources

Atlas Security
Security Advisories

The Atlas security byline: write-ups of vulnerabilities in widely deployed software — what changed, who is exposed, and what to check — verified against primary sources.

Continue reading

More from the Atlas Meridian

Security
16 min read

PHP's October 2026 Critical RCEs: CVE-2026-109618 and CVE-2026-109619

PHP fixed two critical (CVSS 9.2) unauthenticated RCEs in 8.5.12, 8.4.27, 8.3.36, and 8.2.35. Proofs of concept are circulating: patch every PHP server now.

Atlas Security
Security Advisories
View category
Product
15 min read

Why we built Foundry

A developer everything-app — omnibar, sandboxes, calc, HTTP client, todos, notes — built because no single tool did all of it under one hotkey.

Trevor Fannin
Founder and CEO, Atlas Industries
View category
Engage · Atlas Engineering

Reading is one thing. Shipping is another.

If this writeup resonates with what you're building, let's talk. We'll connect you directly with an engineer to assist.