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
| Field | Detail |
|---|---|
| CVE | CVE-2026-111497 |
| Bug | Out-of-bounds write in the Brotli decoder Node bundles in deps/brotli and exposes through node:zlib |
| Severity | High — 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 in | 22.23.4 (Jod LTS) · 24.22.1 (Krypton LTS) · 26.12.1 (Current) |
| Never fixed | Every end-of-life line: 25.x, 23.x, and ≤ 20.x |
| Credit | Reported by Matteo Collina (mcollina) and fixed by Antoine du Hamel (aduh95), per Node.js |
| CVE record | Still 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 line | Status on Oct 21, 2026 | Fixed in | Supported until |
|---|---|---|---|
| 26.x | Current — Active LTS from Oct 28 | 26.12.1 | Apr 30, 2029 |
| 24.x "Krypton" | Maintenance LTS | 24.22.1 | Apr 30, 2028 |
| 22.x "Jod" | Maintenance LTS | 22.23.4 | Apr 30, 2027 |
| 25.x | End-of-life since Jun 1, 2026 | no fix · last release 25.9.0 | — |
| 23.x | End-of-life since Jun 1, 2025 | no fix · last release 23.11.1 | — |
| 20.x "Iron" | End-of-life since Apr 30, 2026 | no fix · last release 20.20.2 | — |
| 18.x "Hydrogen" | End-of-life since Apr 30, 2025 | no fix · last release 18.20.8 | — |
| 16.x and older | End-of-life | no fix | — |
Distro packages, bundled runtimes, and Bun
- Distro packages. Debian's, Alpine's, Fedora's, and RHEL's
nodejspackages 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'sbrotlipackage, and upstream Brotli hasn't tagged a release containing it. Neithernode -vnor 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-alpineandapk add nodejsare different channels. The official image installs Node's own musl build and was rebuilt on October 21.apk add nodejsinstalls 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.versionlogged 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 decodebrresponses. 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.
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.
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 path | Decodes | Default | Off switch |
|---|---|---|---|
Built-in fetch() (undici) | br responses | On | None |
| axios 1.x | br responses | On | decompress: false |
| node-fetch 3.x | br responses | On | compress: false |
| got 16.x | br responses | On | decompress: false |
undici's interceptors.decompress() | br responses | Opt-in | — |
| Express 5 parsers (body-parser 2.x) | br request bodies | On | inflate: 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
zlibover 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:
1PROBE="require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH || 'missing'"2node -p "$PROBE"3# -17 → this build includes the fix4# missing → it doesn'tWe 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
1node -v2which -a node # every node on PATH, in order — often more than one34nvm ls # nvm5fnm list # fnm6volta list node # Volta7asdf list nodejs # asdfTo probe every binary on the PATH in one pass:
1which -a node | while IFS= read -r n; do2 printf '%s %s ' "$n" "$("$n" -v)"3 "$n" -p "$PROBE"4doneVersion 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
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
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
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.
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.
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:
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:
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).
| When | What |
|---|---|
| Oct 27, 2025 | Brotli v1.2.0 — still the latest tagged release |
| Feb 27, 2026 | The fix lands in google/brotli. The commit message doesn't mention security. |
| Mar 24, 2026 | Node 20.20.2, the last Node 20 release, ships without it |
| Apr 30, 2026 | Node 20 reaches end-of-life |
| Oct 14, 2026 | Node.js pre-announces the October security release |
| Oct 21 · ~5:30 AM | 22.23.4, 24.22.1, and 26.12.1 binaries published |
| Oct 21 · 7:08 AM | Advisory updated with CVE details |
| Oct 21 · 9:38–9:45 AM | Rebuilt 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.
| CVE | Severity | Title | Lines |
|---|---|---|---|
| CVE-2026-111497 | High | Node.js Brotli decoder out-of-bounds write permits remote code execution | 26.x, 24.x, 22.x |
| CVE-2026-105482 | High | dns.resolve truncates names containing NUL char | 26.x, 24.x, 22.x |
| CVE-2026-105483 | High | TLS certificate hostname bypass when the IP is written fullwidth digits | 26.x, 24.x |
| CVE-2026-106069 | High | TLS certificate hostname bypass when the host loosely resolves to an IP address | 26.x, 24.x, 22.x |
| CVE-2026-105481 | Medium | Keep-alive socket process a response that was written before the request was sent | 26.x, 24.x, 22.x |
| CVE-2026-106072 | Medium | Improper certificate validation for non-canonical IPv4 hostnames allows DNS SANs to authenticate IP endpoints | 26.x, 24.x, 22.x |
| CVE-2026-106074 | Medium | crypto.randomUUID() and randomInt() entropy caches are saved into user-land startup snapshots and SEAs, so every process repeats the same values | 26.x, 24.x, 22.x |
| CVE-2026-106070 | Low | Node.js ZipFile returns attacker-controlled prefix bytes after failed ZIP header validation | 26.x |
| CVE-2026-106071 | Low | Permission Model filesystem over-grant via wildcard radix lookup | 26.x |
| CVE-2026-106073 | Low | Permission Model bypass with fs.realpathSync | 26.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.
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
- Node.js: October 2026 security releases · release notes for 22.23.4, 24.22.1, and 26.12.1 · end-of-life policy · release schedule
- Code: nodejs/node
67d2947(Brotli cherry-pick) · nodejs/nodea18ec92(zlib constant) · google/brotli0ed4268· Brotli releases - Packaging: Docker official images: node · Docker Hub: node · distroless nodejs
- Bun: Brotli pin in Bun 1.4.3 · pull request #46351 · issue #46352
- Docs: Node.js zlib API · RFC 7932: Brotli Compressed Data Format
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.