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
nodebinary. 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:24andnode:26tags were rebuilt on 21 and 22 October with the fixed releases. Thenode:20andnode:25tags won't be rebuilt. A tag with an exact version, likenode: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
brotlipackage is fixed in 1.2.1 andbrotlicffiin 1.2.1.0. urllib3, and so requests, decodebrresponses 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
brotligem compiles Brotli's C source when it installs. Fixed in 0.9.1. - Go.
github.com/andybalholm/brotliis 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+deb12u1for bookworm,1.1.0-2+deb13u1for 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(ordecode.cpp) forBROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. In the Go port, searchdecode.gofordecoderErrorFormatBlockSwitch.
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
FROMline in Dockerfiles,image:in compose files,.nvmrc,.node-version,.tool-versions, andnode-versionin GitHub Actions workflows. - Python:
requirements*.txt,constraints*.txtand the lock files from Poetry, uv, PDM and Pipenv, forbrotli,brotlicffi,pycurl,pillowandpyarrow. - Go:
go.mod, forgithub.com/andybalholm/brotlibelow v1.2.7. - Ruby:
Gemfile.lock, forbrotlibelow 0.9.1. - npm:
package-lock.json,yarn.lockorpnpm-lock.yaml, foriltorb.
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: brat the proxy or load balancer. This does nothing for an application that fetches Brotli responses from a server an attacker controls. - Taking
brout ofAccept-Encodingisn'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
brotliorbrotlicffipackage 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.
