Broccoli2Shell: how a Brotli decoder bug reaches RCE through Node.js, Python and everything else
Your app can be exposed without calling a Brotli function: HTTP clients in Node and Python decode br responses on their own. Here is how the bug works, where the decoder hides, and the commands to find it.

Google's reference C Brotli decoder, versions 1.0.3 through 1.2.0, has an out-of-bounds write that a crafted compressed stream can trigger. The bug is tracked as CVE-2026-111497 and nicknamed Broccoli2Shell. It sits in DecodeBlockTypeAndLength() in c/dec/decode.c. Attacker bytes overflow an eight-byte internal buffer and corrupt nearby decoder state. That can produce a memory leak or code execution.
The decoder is not the hard part to reach. Node's built-in fetch() decodes Content-Encoding: br responses automatically. In Python, urllib3, and therefore requests, does the same as soon as a Brotli package is installed, whether or not your code ever imports it. Any server your application talks to can deliver the payload.
Fixed upstream releases are 1.0.10, 1.1.1 and 1.2.1. Getting them onto every machine is harder than it sounds, because many runtimes and libraries carry a private copy of the decoder that your distribution's updates never touch.
How attacker bytes reach the decoder
You are exposed if your code, or any library it uses, decompresses Brotli data that someone else controls. The Broccoli2Shell advisory names three paths.
- Outbound HTTP. Node's
fetch(), Python's requests/urllib3 and aiohttp decode Brotli responses on their own. Webhooks, link previews, crawlers and 'import from URL' features all fetch from servers you do not control. - Inbound request bodies. Express 5.0.0 to 5.3.0 and body-parser 2.0.0 to 2.3.0 inflate Brotli request bodies in parsers such as
raw()andjson(). Python frameworks and middleware that decodebrbodies are exposed in the same way. A barenode:httpserver does not decode bodies by itself. - Direct decoder calls. This covers
zlib.brotliDecompress*(),createBrotliDecompress()andbrotli.decompress(). It also covers any library that uses them on archives, caches or queue messages.
Express and body-parser do not contain the bug. They are only routes to it, so upgrading them changes nothing. What matters is which decoder ends up running when the body is inflated.
What goes wrong in DecodeBlockTypeAndLength
A Brotli stream is split into blocks, and the decoder switches between block types as it reads them. Before the fix, DecodeBlockTypeAndLength() returned a plain boolean. Callers could not tell a malformed stream from one that simply needed more input. On the fast path, the decoder ignored the return value altogether.
A crafted stream can request a block switch that cannot happen. The decoder read that as 'need more input' and kept appending attacker bytes to an eight-byte internal buffer, past its end. The overwritten memory is decoder state, which is how a malformed input becomes a primitive for leaking memory or running code. The issue is classified as CWE-252 (unchecked return value) and CWE-787 (out-of-bounds write).
The upstream fix is google/brotli commit 0ed4268, together with 50a9b70, b418fa8 and da98063. The decoder now returns a hard format error to the caller, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17). That constant is also the quickest way to tell whether a vendored copy is patched: if the header defines it, the fix is in.
Python: exposed without importing anything
The PyPI packages Brotli and brotlicffi both compile the same C decoder. You do not have to import either one to be exposed. If one is installed anywhere in the environment, these HTTP clients pick it up:
- urllib3, and so requests, advertises
Accept-Encoding: brand decodes Brotli responses. - aiohttp uses Brotli when it is installed.
- httpx uses it through the
httpx[brotli]extra. - Scrapy and other crawlers. Some versions install Brotli as a dependency, so it can arrive transitively.
This is why a Python service that never mentions Brotli can still be vulnerable. A dependency pulls the package in, urllib3 notices it, and every outbound request now advertises br to whatever server answers.
Do not rely on your scanner here. At disclosure, pip-audit, Dependabot and Safety did not reliably flag the affected PyPI versions, because the CVE record named the upstream Google repository rather than the PyPI packages. GitHub's advisory now lists both, as PYSEC-2026-4322 and PYSEC-2026-4323, but check by hand anyway. If your bindings come from your Linux distribution, for example python3-brotli, the fix arrives through distribution security updates rather than pip.
Node.js: only the Node version matters
Node.js bundles its own copy of Brotli. The system libbrotli is irrelevant to a Node process, so patching the OS package does nothing for it. Fixed releases are 22.23.4, 24.22.1 and 26.12.1. Anything below those on its line is affected, and so is any end-of-life line.
Bun, which also vendors the decoder, had shipped a fix before the advisory's tracking list was compiled. For Node, the practical consequence is that every container base image, CI runner and developer machine with its own Node binary needs checking separately. A host that is patched says nothing about the node: image your service was built on three months ago.
The system library, bindings and vendored copies
The bug is in Google's reference C decoder, not in Node.js or Python. Anything that ships or links Brotli 1.0.3 through 1.2.0 may be affected, even before an advisory names it.
- The system library,
libbrotlidec, packaged aslibbrotli1,brotlior similar. Programs that link it dynamically are fixed once your distribution ships the patch and you restart them. Statically linked or vendored copies need their own rebuild. - Bindings that wrap the C code, such as Java JNI wrappers, PHP and Ruby extensions, and Go cgo bindings. Look for a release that bundles 1.0.10, 1.1.1 or 1.2.1.
- Proxies, CDNs, HTTP clients and archive tools that decode
brcontent with the C library.
The following projects had fixed their vendored copies before the advisory's list was made: Node.js, Bun, Chromium, Firefox, brotlicffi, ngx_brotli, Brotli4j, SumatraPDF, Ghostscript, CatBoost and YTsaurus. Gentoo, Solus and VMware Photon packages were also fixed. The advisory's authors found vendored copies by searching GitHub and reading their headers, last rechecked on 25 October 2026. They say plainly that the list is incomplete. GitHub code search skips submodules and some very large repositories, and private or vendored-at-build-time copies do not show up at all.
Ports rewritten in other languages are a different story. The Java, JavaScript and C# ports used by pdf.js, iText and HtmlUnit cannot reach the bug. The Go port, andybalholm/brotli, had the same logic error, but it could only crash, and it was fixed on 23 October. Rust's brotli-decompressor catches the condition and returns an error. Code execution is only possible in the C decoder.
Checking your versions
Run these on production, staging and CI runners, and inside the container images you deploy. The image matters as much as the host.
Node.js
node --version
# the Brotli version bundled into this Node binary
node -p 'process.versions.brotli'
# inside a container image
docker run --rm --entrypoint node <image> --versionPython
pip show Brotli brotlicffi
# what pulls it in
pipdeptree -r -p Brotli
# lockfiles
grep -i brotli poetry.lock uv.lock requirements*.txt Pipfile.lock 2>/dev/null
# inside a container image
docker run --rm --entrypoint pip <image> show Brotli brotlicffipipdeptree -r is the useful one when pip show reports a package you never asked for. It tells you which dependency dragged Brotli in, and so whether you can drop it.
System library
dpkg -l | grep -i brotli # Debian, Ubuntu
rpm -qa | grep -i brotli # RHEL, Fedora
apk info -v | grep -i brotli # Alpine
# running processes that have the decoder loaded
sudo grep -l brotlidec /proc/*/maps 2>/dev/nullThe /proc/*/maps check closes a gap the package managers leave open. A patched package on disk does not help a long-running process that still has the old libbrotlidec mapped. Any PID that grep returns after the upgrade needs a restart.
Fixing it
The fix depends on where the decoder comes from, and one service can draw on several sources at once.
- Node.js: upgrade to 22.23.4, 24.22.1 or 26.12.1, or later on the same line. Rebuild images on a patched base. Leave end-of-life lines entirely.
- PyPI packages: upgrade
Brotliandbrotlicffito releases carrying the fix, then confirm in the lockfile. If nothing in your code needs Brotli, uninstalling it removes the decoder from the path that urllib3, aiohttp and httpx would otherwise use. - Distribution packages: apply security updates for
libbrotli1/brotliandpython3-brotli. Then restart every process that showed up in the/proccheck. - Vendored or static copies: rebuild against 1.0.10, 1.1.1 or 1.2.1, or cherry-pick
0ed4268. Confirm by checking forBROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCHin the bundled headers.
Upgrading Express or body-parser is not on that list on purpose. It is the most tempting step and the one that does nothing.
Where this guidance runs out
The checks above find decoders you know to look for. They do not find a copy compiled into a closed-source binary, a native extension that statically linked an old libbrotlidec, or a proxy or CDN in front of your service that decodes br on your behalf. For those, you depend on the vendor's advisory. Silence from a vendor is not evidence of a patch.
The vendored-copy list is a snapshot from 25 October 2026 and was built from GitHub code search. That search misses submodules and some large repositories. Treat an unlisted project as unknown, not safe. Also note that being on the list does not mean a project reaches the decoder in every configuration.
Watch your dependency scanners as well. The PyPI advisories exist now, but at disclosure the major Python tools missed them because the CVE pointed at the upstream repository. Similar gaps are likely for other ecosystems that wrap the C code, including PHP, Ruby, JNI and cgo bindings. Verify the bundled Brotli version directly rather than waiting for a green dashboard.
This article draws on What does the "yield" keyword do in Python? and answers to it (1, 2), on the Stack Exchange network, licensed under CC BY-SA 4.0.
- #Broccoli2Shell
- #Brotli vulnerability
- #CVE-2026-111497
- #Node.js fetch Brotli
- #Python
- #urllib3 Brotli



Comments