Overview
CVE-2026-111497 is an out-of-bounds write in Google's Brotli decoder, the C library behind the br in Content-Encoding: br. Google maintains Brotli and issued the CVE; it rates the bug Critical (CVSS 4.0 score 9.2) and lists Brotli 1.0.3 through 1.2.0 as affected, with fixes in 1.2.1, 1.1.1, and 1.0.10. Node.js, which bundles the decoder in every official binary, shipped the first fixes on October 21 in 22.23.4, 24.22.1, and 26.12.1, and rates the bug High. Bun, Envoy, Istio, and the Python Brotli and brotlicffi packages have fixes too (patched versions). The CVE record says the bug "allows a remote unauthenticated attacker to corrupt adjacent decoder state memory, disclose sensitive memory contents, cause a denial of service, or potentially execute arbitrary code using a crafted Brotli-compressed stream."
Is Node2Shell or HTTP2Shell a new vulnerability? No. Both are names for CVE-2026-111497. Imperva calls its analysis Node2Shell, and Aikido Security uses HTTP2Shell for the bug's reach beyond Node.js into software that handles HTTP. It has nothing to do with HTTP/2.
Why it's urgent: Imperva has demonstrated code execution against the official Node.js 24.22.0 build, through both of the paths this post flagged on October 21: fetch() responses and Express 5 request bodies. A public proof of concept reports the same in lab setups. No exploitation in the wild has been reported, and the CVE isn't in CISA's Known Exploited Vulnerabilities (KEV) catalog as of 1:30 PM CDT, October 24 (timeline).
Upgrading is the easy part. Finding every copy of the decoder you run is harder: containers, version managers, CI images, distro packages, bundled apps, and Python environments each carry their own, and node -v on a host describes exactly one of them.
Key facts
| Field | Detail |
|---|---|
| CVE | CVE-2026-111497, "Out-of-Bounds Write in Google Brotli Decoder," issued by Google. Node.js titles it "Node.js Brotli decoder out-of-bounds write permits remote code execution." Advisories: GHSA-9v8h-qqgm-4m35 (Brotli and the PyPI packages), GHSA-663w-cv8g-jj2p (Bun), GHSA-3wvj-j6qm-62mr (Envoy) |
| Severity | Critical, CVSS 4.0 9.2 (AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N), Google's score as the CVE's issuer; Envoy uses the same. Node.js and Bun rate it High. Red Hat rates it Important (CVSS 3.1 8.1). Singapore's Cyber Security Agency cites a CVSS 3.1 score of 9.8, which appears in neither the CVE record nor NVD. |
| Weakness | CWE-787 Out-of-bounds Write · CWE-252 Unchecked Return Value |
| Impact | Memory corruption in the process that decodes the stream: a crash, disclosure of memory contents, or, potentially, code execution, per the CVE record |
| Fixed in | Brotli 1.2.1 · 1.1.1 · 1.0.10 (Oct 21) · Node.js 22.23.4 (Jod LTS) · 24.22.1 (Krypton LTS) · 26.12.1 (Current) (Oct 21) · Bun 1.4.4 (Oct 22) · others |
| Never fixed | Node.js end-of-life lines: 25.x, 23.x, and ≤ 20.x |
| Exploitation | None reported in the wild. Not in CISA's KEV catalog as of 1:30 PM CDT, October 24; CISA's assessment in the CVE record lists exploitation as "none." Imperva has demonstrated code execution, and a public proof of concept exists. |
| CVE records | Published on cve.org by Google on October 22. NVD lists it as "Awaiting Analysis," with no score of its own, as of 1:30 PM CDT, October 24. |
| Credit | Reported by OpenAI Codex Security (Khai Tran and Sylvie Mayer), per the CVE record and Node.js. Fixed upstream by Evgenii Kliuchnikov at Google, and in Node by Matteo Collina (mcollina) and Antoine du Hamel (aduh95). |
Who is affected
Any software that bundles or links Google's Brotli decoder at versions 1.0.3 through 1.2.0 is in the CVE's range. Whether a given program is exposed depends on whether it decompresses Brotli data someone else controls; which applications are exposed maps that out.
Patched versions
| Software | Advisory | Fixed in | Notes |
|---|---|---|---|
| Brotli (google/brotli) | CVE record | 1.2.1, 1.1.1, 1.0.10 | 1.0.3 through 1.2.0 are affected. 1.2.1 is 1.2.0 plus the fix. |
| Node.js | Node.js advisory | 26.12.1, 24.22.1, 22.23.4 | End-of-life lines get no fix; see release lines |
| Bun | GHSA-663w-cv8g-jj2p | 1.4.4 | 1.0.21 through 1.4.3 are affected. Rebuild bun build --compile executables. |
| Envoy | GHSA-3wvj-j6qm-62mr | 1.40.0, 1.39.3, 1.38.6, 1.37.8, 1.36.12 | Affects "Envoy deployments that have Brotli decompressor configured" |
| Istio | ISTIO-SECURITY-2026-007 | 1.31.2, 1.30.6, 1.29.9 | For users "who enable Brotli decompression, for example with an EnvoyFilter." 1.29 is end-of-life. |
Python Brotli (PyPI) | GHSA-9v8h-qqgm-4m35 | 1.2.1 | |
Python brotlicffi (PyPI) | GHSA-9v8h-qqgm-4m35 | 1.2.1.0 | |
| Linux distributions | Each distro's tracker | Varies | Debian and Alpine have fixed brotli; Ubuntu and Red Hat haven't. See status. |
Node.js release lines
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 and bundled runtimes
- 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 arrives through the distro'sbrotlipackage, which other programs link too: on Debian 12,libcurl4depends onlibbrotli1. Debian marks itsnodejspackage "unimportant" for this CVE because "nodejs uses system provided brotli library." Neithernode -vnor the probe proves the decoder is fixed on those installs; your distro'sbrotliadvisory does (status). FROM node:22-alpineandapk add nodejsare different channels. The official image installs Node's own musl build, which bundles its own Brotli, and was rebuilt on October 21; it doesn't install Alpine'sbrotli-libsat all.apk add nodejsinstalls Alpine's package — 24.18.1 on Alpine's edge branch when we checked on October 21 — 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. The same goes for executables built withbun build --compile, which embed the Bun runtime. For third-party software, ask the vendor.
How the vulnerability works
Brotli is a compression format, best known as the br in HTTP's Content-Encoding: br. Google maintains the reference C implementation (RFC 7932), and a lot of software bundles it: Node.js in deps/brotli (exposed through zlib since v11.7.0, backported to v10.16.0), Bun, Envoy, and Python's Brotli and brotlicffi packages among them.
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 the process that calls it, so that write corrupts the process's memory. In Node.js's words, an attacker who can supply Brotli data that an application decompresses "can corrupt process memory, which can lead to a crash, disclosure of process memory, or remote code execution." Code execution runs with the privileges of the process that decoded the data.
To reach it, an attacker needs your application to decompress Brotli data they control. No login is required, and it happens more often than it sounds: Node's built-in fetch() decompresses Brotli responses automatically, Express 5 decompresses request bodies by default, and so do Bun's fetch() and Python's common HTTP clients once Brotli is installed. The next section maps where that happens.
Is it a Node.js bug or a Brotli bug? Brotli's. Google issued the CVE for its Brotli decoder, versions 1.0.3 through 1.2.0, and Node.js is one of several products that ship a copy. For Node, you get the fix by updating Node, not a separate library. Distro packages linked against the system Brotli are the exception, as above. The change that fixes it landed upstream in February, in a commit that doesn't mention security.
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.
On October 21, Google cherry-picked the same commit onto Brotli's three release branches and tagged the fixed releases from them. Each tag adds only the fix and its release preparation.
Node landed the same patch on all three of its 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
Node's advisory names only node:zlib. 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.
Imperva has since confirmed two of these paths. Its proof of concept "ran code carried in a Brotli response or request body inside applications using the official Node.js 24.22.0 Linux arm64 image," through built-in fetch() and through Express 5.0.0–5.3.0 with body-parser 2.0.0–2.3.0. Imperva also notes that "a bare Node.js HTTP server does not decode incoming Brotli bodies simply by receiving them."
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.
Outside Node.js, the same rule holds: exposure follows whatever decodes untrusted Brotli.
- Bun:
fetch(), which asks forbrby default and decodes it automatically, plusnode:zlibandDecompressionStreamwith thebrotliformat, per Bun's advisory. - Python: urllib3 (and so requests), httpx, and aiohttp advertise and decode
brresponses wheneverbrotliorbrotlicffican be imported. Installing either package turns Brotli on for those clients without a code change. That's our reading of urllib3 2.8.0, requests 2.34.2, httpx 0.28.1, and aiohttp 3.14.5. - Envoy and Istio: only where the Brotli decompressor is configured, per their advisories. Envoy's Brotli compressor doesn't decode anything.
- Programs linked against the system library, such as curl built with Brotli support, when they're asked to decode (
curl --compressed).
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 — in Node, Bun, or Python.
- Plain-HTTP fetches, for the reason above.
- Anything that runs
zlibover uploads or third-party payloads.
Red Hat rates the bug lower than Google does. Its reasoning: the vulnerable path needs a malformed stream, hardening such as ASLR "increase[s] the difficulty of turning this memory corruption into reliable code execution," and "Red Hat scores Attack Complexity as High and rates the flaw Important rather than Critical." That changes how hard the bug is to exploit, not whether it's there. Imperva has shown it can be done on a stock build, so a lower rating isn't a reason to wait.
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, and each Python environment carries its own brotli in site-packages. 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 distribution packages
1# Debian and Ubuntu2dpkg-query -W -f='${Package} ${Version}\n' 'libbrotli*'34# RHEL, Fedora, and other RPM-based distros5rpm -qa 'brotli*' 'libbrotli*'67# Alpine8apk list --installed 2>/dev/null | grep brotliCompare the result with your distro's advisory. As of 1:30 PM CDT, October 24:
- Debian: fixed in 12 "bookworm" (
1.0.9-2+deb12u1, DLA-4855-1) and 13 "trixie" (1.1.0-2+deb13u1, DSA-6582-1); unstable isn't fixed yet (tracker). - Alpine:
brotli1.2.1-r0 on edge, 3.24, and 3.23, and 1.1.1-r0 on 3.21; no fix is listed for 3.22. Alpine lists itsnodejspackage as not affected, since it uses the system library, andnodejs-currentas fixed in 26.12.1-r0 (tracker). - Ubuntu: every release is "Needs evaluation" for both
brotliandnodejs, and no Ubuntu Security Notice has been published (tracker). - Red Hat: rated Important. RHEL 8, 9, and 10
brotliand thenodejsmodule streams are listed as affected, and only Red Hat Hardened Images carry a fix (CVE page). Red Hat lists more than 100 affected entries in all, including Firefox, Thunderbird, and theistio-proxyin OpenShift Service Mesh. Fedora's bugs are open (Bugzilla).
A package update doesn't reach processes that already loaded the old library. This lists every process still mapping a Brotli copy that an upgrade replaced, whether the system library or a Python extension module (run it as root to see every process):
1for f in $(grep -l 'brotli.*(deleted)' /proc/[0-9]*/maps 2>/dev/null); do2 p=${f#/proc/}; p=${p%/maps}3 printf '%s %s\n' "$p" "$(tr '\0' ' ' < /proc/"$p"/cmdline)"4doneWe tested it in a Debian 13 container: after libbrotli1 was replaced and brotli upgraded with pip, it listed both the process using the system library and the Python process still holding the old module. Restart whatever it lists.
Check Python, Bun, Envoy, and Istio
1# Python, in each virtualenv, container, and CI image2python3 -m pip list 2>/dev/null | grep -iE '^(brotli|brotlicffi) '3# brotli 1.2.1 and brotlicffi 1.2.1.0 are the first fixed versions45# Bun6bun --version # 1.4.4 or later78# Envoy: is the Brotli decompressor configured?9grep -rn 'brotli.decompressor' /etc/envoy/1011# Istio: EnvoyFilters that enable Brotli decompression12kubectl get envoyfilters -A -o yaml | grep -n 'brotli.decompressor'The pattern matches both Envoy's extension name (envoy.compression.brotli.decompressor) and its type URL, and skips the Brotli compressor. We tested the pip check in a python:3.13-slim container before and after upgrading, and the patterns with GNU grep 3.8 against fixture Envoy configs and EnvoyFilter output. Compare Envoy's and Istio's versions with the patched versions either way: a decompressor can be added later.
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. For Python images, run the pip list check the same way, with --entrypoint python3.
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. For Python projects, look for brotli and brotlicffi pins in lockfiles and requirements files, or run the pip list check inside each CI image.
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> .Then restart every Node process: a running process keeps the old binary until it exits.
On distro-packaged Node, the fix comes from the distro's brotli package. Debian and Alpine have shipped it; Ubuntu and RHEL haven't, as of 1:30 PM CDT, October 24 (status). If a host needs the fix before its distro ships one, move it to the official nodejs.org binaries or the official Docker images.
Update Brotli everywhere else
1# Python: upgrade whichever of the two is installed, in every environment2python3 -m pip install -U brotli # 1.2.1 or later3python3 -m pip install -U brotlicffi # 1.2.1.0 or later45# Bun on a host; in containers, rebuild from oven/bun:1.4.4 or later6bun upgrade78# Debian and Ubuntu, once your release has a fix9sudo apt-get update && sudo apt-get install --only-upgrade libbrotli11011# Alpine12apk upgrade brotli-libsPin the fixed versions (brotli>=1.2.1, brotlicffi>=1.2.1.0) so a rebuild can't fall back, and rebuild any bun build --compile executables with Bun 1.4.4 or later. We tested the pip, apt-get, and apk commands in containers; the apt-get line took Debian 12 from 1.0.9-2+b6 to 1.0.9-2+deb12u1. For Envoy and Istio, move to the patched versions; Istio's sidecars pick up the new proxy only when their workloads restart. After any of these, restart what the process check lists.
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. Bun's advisory offers one per request: pass decompress: false to fetch(), and Bun neither asks for nor decodes br. The other 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.
In Python, urllib3, requests, httpx, and aiohttp only decode br when brotli or brotlicffi can be imported, so removing both from an environment that doesn't otherwise need them turns Brotli off. Check that nothing in the environment depends on them first.
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."
When the fix shipped, Node 20 had 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 and exploitation status
Dates and times come from Brotli's releases, the Node.js release archive, each vendor's advisory, PyPI, and Docker Hub. All times are CDT (UTC−5).
| When | What |
|---|---|
| Oct 27, 2025 | Brotli v1.2.0 released |
| 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 6, 2026 | Envoy 1.39.3, 1.38.6, 1.37.8, and 1.36.12 ship. Their release notes don't mention Brotli; Envoy's advisory later lists them as fixed. |
| Oct 13, 2026 | Envoy 1.40.0 ships |
| Oct 14, 2026 | Node.js pre-announces the October security release |
| Oct 15, 2026 | Istio publishes ISTIO-SECURITY-2026-007 with 1.31.2, 1.30.6, and 1.29.9; it adds CVE-2026-111497 to the bulletin on Oct 20 |
| Oct 21 · ~5:30 AM | Node.js 22.23.4, 24.22.1, and 26.12.1 binaries published |
| Oct 21 · 7:08 AM | Node.js 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 |
| Oct 21 · 2:26–2:27 PM | Brotli 1.0.10, 1.1.1, and 1.2.1 published |
| Oct 21 · 11:08 PM | A public proof-of-concept repository is created |
| Oct 22 · 5:11 AM | Bun 1.4.4 ships with Brotli 1.2.1 |
| Oct 22 · 7:10 AM | Node.js updates the credit to OpenAI Codex Security |
| Oct 22 · 7:22 AM | Imperva publishes its Node2Shell analysis |
| Oct 22 · 8:35 AM | Google publishes the CVE record, with a CVSS 4.0 score of 9.2 |
| Oct 22 · 8:57 AM | Python Brotli 1.2.1 on PyPI; brotlicffi 1.2.1.0 follows at 11:19 AM |
| Oct 22 · 10:32 AM | GitHub publishes GHSA-9v8h-qqgm-4m35 for the PyPI packages |
| Oct 23 · 4:31 AM | Bun publishes GHSA-663w-cv8g-jj2p |
| Oct 23 · 11:03 AM | Envoy publishes GHSA-3wvj-j6qm-62mr |
| Oct 23 | Singapore's Cyber Security Agency issues alert AL-2026-154; Debian issues DSA-6582-1 and DLA-4855-1; Aikido Security starts calling the bug HTTP2Shell |
Exploitation status. No exploitation in the wild has been reported, and CVE-2026-111497 isn't in CISA's KEV catalog as of 1:30 PM CDT, October 24. CISA's assessment in the CVE record, dated October 23, lists exploitation as "none" and the bug as not automatable. Imperva says its standalone proof of concept "needed no debugger or instrumented binary," but that "execution took multiple attempts in our setup." The public proof-of-concept repository's own description reports code execution in three Linux x86-64 lab setups and calls its Express result "profile specific and probabilistic," dependent on a pinned build and a guessed memory address. We haven't tested any public proof of concept, and we aren't linking them.
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.
Tagged Brotli releases followed Node's by about nine hours, so software built from a tagged release picks up the fix only once it moves to 1.2.1, 1.1.1, or 1.0.10.
Other vulnerabilities fixed in the Node.js release
Node's October 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: an NVD score; fixes from Ubuntu, Red Hat, or other distributions; advisories from other software that bundles Brotli; a KEV listing or any report of exploitation.
- October 24, 2026, 1:30 PM CDT: Google published CVE-2026-111497 for its Brotli library, so this post now covers Brotli and the other software that bundles it. Added Google's Critical (CVSS 9.2) rating next to Node's High and the other authorities' ratings, the tagged Brotli releases, the fixes from Bun, Envoy, Istio, and the Python packages, distribution status, Imperva's demonstration and the public proof of concept, and checks for distribution packages, Python, Bun, Envoy, and Istio. Corrected the credit, which Node.js changed on October 22, and the entry on Bun, which was affected and fixed the bug in 1.4.4. The post moved from
/blog/cve-2026-111497-nodejs-brotli-decoder; old links redirect here.
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
- Google Brotli: CVE-2026-111497 record · GHSA-9v8h-qqgm-4m35 · releases 1.2.1, 1.1.1, and 1.0.10 · commits
0ed4268,b418fa8,da98063, and50a9b70 - 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 · commits
67d2947(Brotli cherry-pick) anda18ec92(zlib constant) - Other vendors: Bun GHSA-663w-cv8g-jj2p · Brotli pin in Bun 1.4.4 · Envoy GHSA-3wvj-j6qm-62mr · ISTIO-SECURITY-2026-007 · PyPI: Brotli and brotlicffi
- CVE records and agencies: cve.org · NVD · CISA KEV catalog · CSA Singapore AL-2026-154
- Distributions: Debian · Ubuntu · Alpine · Red Hat · Red Hat Bugzilla
- Research and reporting: Imperva: Node2Shell · Aikido Security · oss-security thread
- Packaging: Docker official images: node · Docker Hub: node · distroless nodejs
- 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.