A poster of a bakery yard run by machines. A placard hanging in the middle shows the Node.js logo and CVE-2026-111497. Robots sort bread rolls into crates, and one roll in an open crate has a red crack in it. A sign on the right reads Arvion.

CVE-2026-111497 · Brotli

Node2Shell isn't only in Node:how to find every copy of Brotli

Antonio Firmiano

CEO · · 6 min read

Node.js released fixes on 21 October for CVE-2026-111497, a bug in Google's Brotli decoder that goes by several names: Node2Shell, HTTP2Shell, Brotli2Shell and Broccoli2Shell. If you updated Node.js, you fixed Node's copy of Brotli. Node.js doesn't share that copy with anything else. Python wheels, Ruby gems, Go modules, Bun, .NET, browsers, firewalls and your operating system each carry their own, and each one gets the fix only when whoever builds it rebuilds it.

This post covers where those copies are, a one-line check that tells a fixed build from an old one, and which popular packages still ship the old decoder as of 27 October.

What the bug is

Brotli is the compression format behind Content-Encoding: br. Brotli 1.0.3 to 1.2.0 have an out-of-bounds write in the decoder: malformed compressed data makes it write past an 8-byte buffer. Google released Brotli 1.2.1, 1.1.1 and 1.0.10 with the fix on 21 October, the same day as the Node.js security releases. The CVE record scores it 9.2 under CVSS 4.0. The code change itself (0ed4268) went into Brotli's repository on 27 February, so a copy built from Brotli's main branch after that date already has it.

Whether it leads to code execution in practice is still being worked out. Imperva got code execution against the official Node.js 24.22.0 image on Linux arm64 after several attempts and hasn't released its exploit. ELLIO reproduced it on x86-64 against a plain node:http server, Express 5.3.0 and Koa 3. It says the exploit depends on memory offsets that differ between Node.js builds, and that most of its attempts against Express crashed the worker instead. A crash is a denial of service on its own. On 26 October ELLIO reported an exploitation attempt on its sensors aimed at Node.js 24.22.0 on x86-64, which Tenable notes it hasn't confirmed. As of CISA's 26 October catalog, the CVE isn't in the Known Exploited Vulnerabilities list.

Where the copies are

Brotli is a small C library, and most projects compile it into their own binaries instead of using the one your operating system provides. These are the places it turns up, with the first version that has the fix:

  • Node.js builds Brotli into the node binary. Fixed in 22.23.4, 24.22.1 and 26.12.1. Node.js 20, 23 and 25 are past end of life and get no fix.
  • Node.js Docker images. The official node:22, node:24 and node:26 tags were rebuilt on 21 and 22 October with the fixed releases. The node:20 and node:25 tags won't be rebuilt. A tag with an exact version, like node:24.22.0-bookworm-slim, or a digest pin stays on the old build until you change it.
  • Bun builds in its own copy. Fixed in 1.4.4.
  • Python. The brotli package is fixed in 1.2.1 and brotlicffi in 1.2.1.0. urllib3, and so requests, decode br responses through one of those two packages when it's installed (source). Other wheels bundle their own Brotli library: Pillow for WOFF2 fonts, pyarrow for Parquet files, pycurl and curl_cffi for HTTP responses, and PyMuPDF.
  • Ruby. The brotli gem compiles Brotli's C source when it installs. Fixed in 0.9.1.
  • Go. github.com/andybalholm/brotli is a translation of the C code into Go and has the same bug. Fixed in v1.2.7.
  • .NET 8.0.33, 9.0.22 and 10.0.14 (Microsoft).
  • Envoy and Istio. Tenable lists the fixed releases as Envoy 1.36.12, 1.37.8, 1.38.6, 1.39.3 and 1.40.0, and Istio 1.29.9, 1.30.6 and 1.31.2.
  • Firefox 150 and ESR 140.10 (Mozilla bug). Electron has fixed releases on its 43, 44 and 45 lines.
  • Firewalls. Palo Alto Networks lists PAN-OS 12.2, 12.1 and 11.2 as affected, with hotfix builds and a workaround that turns off Brotli inspection.
  • Your operating system's libbrotli. Debian fixed it in DSA-6582-1 (1.0.9-2+deb12u1 for bookworm, 1.1.0-2+deb13u1 for trixie) and Ubuntu in USN-8956-1. A release past its end of life gets no update. Ubuntu 25.10, for example, ended on 1 July 2026 (endoflife.date).

Check a compiled copy with one line

The fix added a new error code to Brotli, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. Brotli keeps the names of its error codes in the compiled library so it can print them, so a build made from the fixed source contains the text BLOCK_SWITCH, and an older build doesn't. Another name, EXUBERANT_NIBBLE, is in every version, so it tells you whether the file keeps the names at all.

f=$(command -v node)
if ! grep -q EXUBERANT_NIBBLE "$f"; then echo "no Brotli error names, can't tell"
elif grep -q BLOCK_SWITCH "$f"; then echo "fixed"
else echo "old decoder"
fi

Set f to any binary or library: node, bun, a Python extension module, a libbrotlidec file, a .dll or a .exe. To check every library under a folder, such as a virtualenv, node_modules or /usr/lib, run this from inside it:

grep -rl --include='*.so*' --include='*.node' --include='*.dylib' \
  --include='*.dll' --include='*.pyd' --include='*.bundle' --include='*.exe' \
  EXUBERANT_NIBBLE . 2>/dev/null |
while read -r f; do
  if grep -q BLOCK_SWITCH "$f"; then echo "fixed        $f"
  else echo "old decoder  $f"; fi
done

We ran both on Linux against Node.js 22 and 24 for Linux, macOS and Windows before and after the fix, Bun 1.4.3 and 1.4.4, the Python brotli 1.2.0 and 1.2.1 wheels, the wheels in the table below, Debian's fixed libbrotli1 packages for bookworm and trixie, and the Brotli 1.0.7 library inside Playwright's WebKit build. Wherever the release or the file name told us the Brotli version, the answer matched it. Debian's backported packages carry the new name too, so the check works on distribution packages that keep the old version number.

The check has limits:

  • Some builds don't keep the names, so it prints "can't tell". DuckDB's Python wheel is one. For those, check the version its maker ships. Trail of Bits' brotli-checker looks for the decoder's compiled code in Linux binaries, so it can also judge builds without the names.
  • Brotli 1.0.0 to 1.0.2, from 2017, came before the bug but also lack the new name, so they show "old decoder". They're old enough to replace anyway.
  • For vendored source code, search the copy of decode.c (or decode.cpp) for BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. In the Go port, search decode.go for decoderErrorFormatBlockSwitch.

Packages still shipping the old decoder

We downloaded the latest release of popular packages that bundle Brotli and ran the check on the files inside, without installing or running them. As of 27 October:

Package Latest release What's inside Status
Pillow 13.0.0 Old decoder in pillow.libs/libbrotlidec Update merged on 27 October. The next planned release is 13.1.0 on 2 January 2027.
pyarrow 26.0.0 Old decoder in libarrow Tracked in apache/arrow#55165.
DuckDB 2.0.0 The vendored source in 2.0.0 predates the fix Fix in pull request #27791, not merged yet.
iltorb (npm) 2.4.5, from 2020 Old decoder Deprecated. Its notice says to use the zlib module that Node.js has had since 10.16.0.
pycurl 7.49.0 Brotli 1.2.1 Fixed on 27 October. 7.48.0 has the old decoder.
curl_cffi 0.17.0 Fixed
PyMuPDF 2.0 Fixed

Pillow and pyarrow are downloaded about 240 million times a week from PyPI (pypistats), and upgrading them won't help yet. The decoder runs only when they read Brotli-compressed data: WOFF2 fonts in Pillow, and Brotli-compressed Parquet files and streams in pyarrow. So the question for each project is whether it opens those from people it doesn't trust.

Where the versions are pinned

Each copy is pinned somewhere in your repositories, often in several places:

  • Node.js: the FROM line in Dockerfiles, image: in compose files, .nvmrc, .node-version, .tool-versions, and node-version in GitHub Actions workflows.
  • Python: requirements*.txt, constraints*.txt and the lock files from Poetry, uv, PDM and Pipenv, for brotli, brotlicffi, pycurl, pillow and pyarrow.
  • Go: go.mod, for github.com/andybalholm/brotli below v1.2.7.
  • Ruby: Gemfile.lock, for brotli below 0.9.1.
  • npm: package-lock.json, yarn.lock or pnpm-lock.yaml, for iltorb.

A Docker image built before the fix keeps its old copy until you rebuild it. With a fixed tag in your Dockerfile, rebuild with --pull so Docker fetches the new base instead of reusing the one it already has.

If you can't update yet

Tenable's FAQ lists these stopgaps:

  • Block requests marked Content-Encoding: br at the proxy or load balancer. This does nothing for an application that fetches Brotli responses from a server an attacker controls.
  • Taking br out of Accept-Encoding isn't enough on its own, because a hostile server can still reply with Brotli. Turning off Brotli compression on your origin or CDN doesn't change decompression at all.
  • In Envoy and Istio, turn off the Brotli decompressor filter in both directions. A disabled filter passes the compressed bytes through, so a service behind it that decodes Brotli is still exposed.
  • In Python, remove the brotli or brotlicffi package until a fixed version is in place.

On PAN-OS, Palo Alto's advisory gives a command that turns off Brotli inspection.

OpenSourceMalware's write-up collects more of the projects that carry Brotli, if you want to look beyond the ones above.

Is Brotli spread across your repositories?

This post covers the copies you can check by hand. Your repositories pin Node.js, Python, Ruby and Go packages that each carry their own. Book 15 minutes with the Arvion team to go through them.

Book 15 min