Node2Shell: closing the Brotli flaw CVE-2026-111497 in Node.js, Python and .NET
On 21 October 2026 Node.js fixed a flaw in the Brotli decoder it ships with. It is now clear that the bug lives in Google's Brotli library itself and reaches far beyond Node.js, into Python packages, .NET, Bun, Envoy and the packages of Linux distributions. Whether you are affected depends less on a version number than on whether your software decompresses Brotli data from an untrusted source. This article shows where that happens, which versions are safe and how to check your systems with tested one-liners.
Key takeaways
- What: an out-of-bounds write in the Brotli decoder of Google Brotli 1.0.3 to 1.2.0,
CVE-2026-111497, nicknamed "Node2Shell" and also "Broccoli2Shell". - Rating: CVSS 4.0 9.2 from Google as the assigning CNA; Microsoft and Red Hat arrive at 8.1 under CVSS 3.1, and Node.js rates it "High".
- Impact: crashes, process memory disclosure and, in the lab, code execution as soon as an application decompresses crafted Brotli data.
- Status: as of 26 October no exploitation in the wild is known and the CVE is not in CISA's KEV catalogue; a public exploit only works against a deliberately weakened build.
- Fix: Brotli 1.0.10, 1.1.1 or 1.2.1; Node.js 22.23.4, 24.22.1 or 26.12.1; Python Brotli 1.2.1 or brotlicffi 1.2.1.0; .NET 8.0.33, 9.0.22 or 10.0.14. Then restart processes and rebuild images.
What happened?
The bug is old; the disclosure was quick. According to Aikido, OpenAI found it as early as February 2026, and the fix landed in the public Brotli repository back then without being labelled a security fix. Projects that bundle their own copy of the Brotli code therefore never picked it up. Node.js credits the OpenAI Codex Security team (Khai Tran and Sylvie Mayer) with the report; Matteo Collina and Antoine du Hamel built the fix.
- February 2026: the fix lands in the Brotli source code without a security label.
- 21 October: Node.js releases 22.23.4, 24.22.1 and 26.12.1 with ten security fixes, this one among them. Google releases Brotli 1.0.10, 1.1.1 and 1.2.1. Vercel deploys WAF rules, and the official Node.js Docker images are updated.
- 22 October: NVD publishes CVE-2026-111497 and GitHub publishes advisory GHSA-9v8h-qqgm-4m35; Brotli 1.2.1 reaches PyPI. Imperva demonstrates code execution on the official Node.js 24.22.0 image (Linux, arm64) and withholds the exploit. A first public proof of concept achieves code execution only against a deliberately weakened build. Bun 1.4.4 ships.
- 23 October: brotlicffi 1.2.1.0 ships, and the Python packages get their own advisories (PYSEC-2026-4322 and PYSEC-2026-4323). Envoy and Istio release fixes.
- 24 October: the Ruby gem 0.9.1, the PHP extension 0.21.1 and brotli4j 1.24.0 for Java follow; Amazon Linux 2023 publishes ALAS2023-2026-3334.
- 25 October: Microsoft ships out-of-band .NET 8.0.33, 9.0.22 and 10.0.14 plus .NET 11 RC3 and, according to SecurityWeek, updates for Visual Studio 2019, 2022 and 2026 and for PowerShell 7.4 to 7.6.
- 26 October: Ubuntu publishes USN-8956-1 for 22.04 to 26.10; Debian has fixed packages with DSA-6582-1 (trixie) and DLA-4855-1 (bookworm).
Context: As of 26 October no exploitation in the wild is known. CISA has not added the flaw to its KEV catalogue; its assessment rates it not automatable but with total technical impact. Microsoft considers exploitation "more likely". NVD has not scored the flaw itself yet, and some scanners show 9.8 under CVSS 3.1. The differing scores reflect that an attack only works if the application decompresses untrusted Brotli data. If yours does, don't wait for the first break-in.
The flaw in plain terms
Brotli compresses data in blocks, and a stream may switch between block types. The function
DecodeBlockTypeAndLength() in c/dec/decode.c used to report nothing more than
"worked" or "didn't work". The second answer could mean two things: the input hasn't fully arrived yet, or
the stream asks for a switch that cannot exist, for example in a category with only one block type.
The streaming decoder treated both as "needs more input" and kept appending bytes to an eight-byte
internal buffer, past its end. The fast path ignored the result altogether and carried on decoding from an
impossible state. A crafted stream thus overwrites the decoder's adjacent state. The fix introduces a
dedicated error code, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17), which
every caller now returns. In formal terms these are two weaknesses: an unchecked return value (CWE-252) and
an out-of-bounds write (CWE-787).
Why it reaches so far: Brotli sits behind Content-Encoding: br in
browsers, HTTP libraries and runtimes. Many projects ship the C code as their own copy, among them
Node.js, .NET and the Python packages on PyPI. Your distribution's update does not touch such a copy;
each one needs its own update.
Three paths to the decoder
An affected Brotli version on disk is not yet an attack. It becomes dangerous once data from an untrusted source reaches the decoder, and there are three ways for that to happen:
Inbound: the server decompresses requests
A client sends a body with Content-Encoding: br, and the application decompresses it before
processing. Express 5.0.0 to 5.3.0 and body-parser 2.0.0 to 2.3.0 do this with json(),
raw(), text() and urlencoded(), as does ASP.NET Core with request
decompression enabled for br and Python frameworks with matching middleware. A bare
node:http server decompresses nothing by itself, and Express 4 and body-parser 1.x
don't support Brotli request bodies. In .NET, request decompression is off by default.
Outbound: the application fetches foreign content
The application fetches a URL and decompresses the response automatically. Node.js's fetch()
does so without any extra setting; urllib3, and therefore requests, as well as aiohttp and httpx do so as
soon as the Python package Brotli or brotlicffi is installed; in .NET it is HttpClient when
AutomaticDecompression includes Brotli or All. Typical triggers are webhooks, link
previews, crawlers, "import from URL" features and AI agents that read web pages. A web application firewall
in front of your server never sees these responses. Dropping br from
Accept-Encoding helps little, because a malicious server sends Brotli anyway.
Direct: your own code decompresses data
Code that runs zlib.brotliDecompress(), brotli.decompress() or
BrotliStream on files, caches or messages from a queue is affected as soon as that data does
not come from a trusted source.
From our own inventory: A popular chat front end for local language models shows how
quietly Brotli comes along. In the image ghcr.io/open-webui/open-webui:v0.11.4-slim we found,
on 26 October, the Python package Brotli 1.2.0 next to aiohttp, httpx and requests, plus the system
library libbrotli1 1.0.9-2+b6 on Debian 12. Both are in the affected range. How much
risk that carries depends on whether the instance may fetch foreign URLs, for web search or loading web
pages, say. Our own services on python:3.14.7-slim-trixie, built on the standard library
alone, contain no Brotli decoder at all.
Who is affected?
Any software that loads or bundles the C decoder of Brotli 1.0.3 to 1.2.0 is affected. The most important cases as of 26 October 2026:
| Environment | Affected | Fixed in | Note |
|---|---|---|---|
| Node.js 22 · 24 · 26 | yes, decoder in the runtime | 22.23.4 · 24.22.1 · 26.12.1 | Node.js 20, 21, 23 and 25 get no fix. The Ubuntu and Debian Node.js packages use the system library. |
| Bun | yes | 1.4.4 | via fetch(), node:zlib and DecompressionStream |
Python Brotli · brotlicffi | yes | 1.2.1 · 1.2.1.0 | takes effect through requests/urllib3, aiohttp, httpx[brotli], Scrapy; often pulled in as an indirect dependency |
| .NET 8 · 9 · 10 | yes, not reachable by default | 8.0.33 · 9.0.22 · 10.0.14 | .NET Framework not affected, .NET 7 without a fix; republish self-contained and NativeAOT apps |
| Visual Studio · PowerShell | yes | updates of 25 October | Visual Studio 2019, 2022, 2026; PowerShell 7.4, 7.5, 7.6 |
System library libbrotli | yes | see next table | programs that load it need a restart after the update |
| Envoy · Istio | with the Brotli decompressor filter | Envoy 1.40.0, 1.39.3, 1.38.6, 1.37.8; Istio 1.29.9, 1.30.6, 1.31.2 | remove the filter from your EnvoyFilters until updated |
| Java | only JNI wrappers around the C code | brotli4j 1.24.0 | pure Java ports not affected |
| PHP · Ruby | extension or gem | PHP extension brotli 0.21.1 · gem 0.9.1 | copies bundled in other projects stay affected until updated |
| Go · Rust | Go port only crashes, Rust catches the case | andybalholm/brotli since 23 October | cgo bindings around the C code are affected |
Apache mod_brotli | no | not needed | compresses only, never decompresses |
| Chromium · Firefox | fixed | per broccoli2shell.com | update browsers as usual |
| DuckDB, Apache Arrow, 7-Zip-zstd, p7zip, EDK II | yes | still open | per the overview on broccoli2shell.com, as of 26 October |
Trap: the wrong update. npm update brotli or a newer Express changes
nothing, because the vulnerable code lives in the Node.js runtime. apt upgrade on the host
does not close the flaw in a container image that brings its own runtime; an image on
node:24.22.0 stays vulnerable until it is rebuilt. Running processes keep the old decoder in
memory until they restart. And process.versions.brotli is no proof: Node.js cherry-picked the
fix, so the Brotli version it reports does not necessarily change. The Node.js version is what counts.
Safe versions
Fixed versions of the system library and the runtimes as of 26 October 2026. Compare them with the package version from your source:
| Source | Package | Fixed in | Note |
|---|---|---|---|
| Google Brotli (upstream) | 1.0 · 1.1 · 1.2 | 1.0.10 · 1.1.1 · 1.2.1 | fix from commit 0ed4268 |
| Ubuntu 26.10 | brotli, libbrotli1, python3-brotli | 1.2.0-4ubuntu0.1 | USN-8956-1 |
| Ubuntu 26.04 LTS | as above | 1.2.0-3ubuntu0.1 | USN-8956-1 |
| Ubuntu 24.04 LTS | as above | 1.1.0-2ubuntu0.1 | USN-8956-1 |
| Ubuntu 22.04 LTS | as above | 1.0.9-2ubuntu0.1 | USN-8956-1 |
| Debian 13 trixie | brotli | 1.1.0-2+deb13u1 | DSA-6582-1 |
| Debian 12 bookworm | brotli | 1.0.9-2+deb12u1 | DLA-4855-1 (Debian LTS) |
| Alpine | brotli | 1.2.1-r0 (edge, 3.23, 3.24) · 1.1.1-r0 (3.21, 3.22) | per Aikido |
| Amazon Linux 2023 | brotli | 1.1.0-1.amzn2023.0.2 | ALAS2023-2026-3334 |
| RHEL 8 · 9 · 10 | brotli | partly | Red Hat: "Important", CVSS 3.1 8.1; bulletin RHSB-2026-015 |
| Fedora 43 to 45 · SUSE | brotli | still open | per Aikido on 26 October |
| Node.js (official builds) | 22 · 24 · 26 | 22.23.4 · 24.22.1 · 26.12.1 | official Docker images too, since 21 October |
| Python (PyPI) | Brotli · brotlicffi | 1.2.1 · 1.2.1.0 | PYSEC-2026-4322, PYSEC-2026-4323 |
| .NET runtime | 8 · 9 · 10 | 8.0.33 · 9.0.22 · 10.0.14 | SDKs 8.0.133/8.0.427, 9.0.123/9.0.320, 10.0.114/10.0.403 |
1.Take stock
Start with the systems that fetch foreign URLs or accept compressed requests. Each of the following
commands prints a finished result, OK, VULNERABLE or a hint per finding. We tested
all of them against real images; an older node:22-alpine, for instance, reported
v22.23.3 VULNERABLE.
node -e 'const v=process.versions.node.split(".").map(Number),f={22:[23,4],24:[22,1],26:[12,1]}[v[0]];console.log(process.version,!f?"NO FIX (end of life)":(v[1]>f[0]||(v[1]==f[0]&&v[2]>=f[1]))?"OK":"VULNERABLE")'
# same check inside an image: docker run --rm --entrypoint node <image> -e '...'
python3 - <<'EOF'
from importlib.metadata import version, PackageNotFoundError
fixed = {"Brotli": (1, 2, 1), "brotlicffi": (1, 2, 1, 0)}
for name, good in fixed.items():
try:
v = version(name)
except PackageNotFoundError:
print(name, "not installed")
continue
parts = tuple(int(p) for p in v.split(".")[:len(good)] if p.isdigit())
print(name, v, "OK" if parts >= good else "VULNERABLE")
EOF
# inside a container: docker exec -i <container> python3 - <<'EOF' ... EOF
On Debian and Ubuntu, check the system library against the fixed version for your release from the table,
here for Debian 12. The second command counts the processes that still hold a replaced
libbrotlidec in memory; 0 means everything has been reloaded:
dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' 'libbrotli1*' 2>/dev/null \
| awk '$1=="ii"{r=system("dpkg --compare-versions "$3" ge 1.0.9-2+deb12u1"); print $2, $3, (r==0?"OK":"VULNERABLE")}'
sudo grep -ls 'libbrotlidec.*(deleted)' /proc/[0-9]*/maps 2>/dev/null | wc -l
For .NET, what counts is the shared runtime Microsoft.NETCore.App, which an ASP.NET Core
application under IIS loads too. On Windows and Linux:
$fix = @{8=33; 9=22; 10=14}; dotnet --list-runtimes | ForEach-Object { if ($_ -match '^Microsoft\.NETCore\.App (\d+)\.(\d+)\.(\d+)') { $maj = [int]$Matches[1]; $p = [int]$Matches[3]; $s = if ($maj -lt 8) { 'NO FIX (end of life)' } elseif (-not $fix.ContainsKey($maj)) { 'CHECK' } elseif ($p -ge $fix[$maj]) { 'OK' } else { 'VULNERABLE' }; "Microsoft.NETCore.App $($Matches[1]).$($Matches[2]).$p $s" } }
dotnet --list-runtimes | awk '$1=="Microsoft.NETCore.App"{split($2,v,".");f=(v[1]==8?33:v[1]==9?22:v[1]==10?14:-1);print $1,$2,(v[1]<8?"NO FIX (end of life)":f<0?"CHECK":(v[3]+0>=f?"OK":"VULNERABLE"))}'
A search of the source code shows whether your own code decompresses untrusted Brotli data. A hit is not a vulnerability, just the place to look first:
# .NET: request decompression, HttpClient decompression, direct decoder use
grep -rnE --include='*.cs' 'AddRequestDecompression|DecompressionMethods\.(All|Brotli)|BrotliStream|BrotliDecoder' .
# Node.js: direct decoder calls, and Express 5 / body-parser 2 in the dependency tree
grep -rnE --include='*.js' --include='*.ts' --exclude-dir=node_modules 'brotliDecompress|createBrotliDecompress' .
npm ls express body-parser --all 2>/dev/null | grep -cE 'express@5|body-parser@2'
2.Patch, restart, rebuild
The order is always the same: update, restart running processes, rebuild images. Skip the second or third step and the old decoder stays in production.
sudo apt update
sudo apt install --only-upgrade libbrotli1 brotli python3-brotli # skips what is not installed
# restart the services that still map the old library, until this prints 0
sudo grep -ls 'libbrotlidec.*(deleted)' /proc/[0-9]*/maps 2>/dev/null | wc -l
python3 -m pip install -U "Brotli>=1.2.1"
python3 -m pip install -U "brotlicffi>=1.2.1.0"
# then regenerate lockfiles (requirements.txt, poetry.lock, uv.lock) and rebuild images
- Node.js: move to 22.23.4, 24.22.1 or 26.12.1 and restart every Node.js process. Switch images that point at a fixed tag such as
node:24.22.0to the fixed tag and rebuild them; the same goes for single executable applications and startup snapshots. Node.js 20, 21, 23 and 25 get no fix: move to 22 or 24. - .NET under IIS: install the Hosting Bundle or runtime 8.0.33, 9.0.22 or 10.0.14 and restart the application pools, for example with
Restart-WebAppPool -Name <pool>oriisreset. Republish self-contained applications with the updated SDK; with NativeAOT, updating the runtime on the server does not replace the decoder linked into the executable. - .NET in containers: pull the images from
mcr.microsoft.com/dotnetagain and rebuild the application. - Containers in general: an update on the host never reaches the image. Rebuild and redeploy every image with its own runtime or its own
libbrotlion a patched base; where images are pinned by digest, move the pin deliberately.
3.Stopgaps until you can patch
Until the update is live, blocks in two places help. They do not replace the update, as Microsoft and Imperva both stress. Test every block, because it changes how requests and responses are handled.
Inbound: reject Brotli bodies at the proxy
# nginx (server or location block): reject Brotli request bodies before the app decodes them
if ($http_content_encoding ~* "br") { return 415; }
# HAProxy (frontend)
http-request deny deny_status 415 if { req.hdr(content-encoding) -m sub -i br }
<system.webServer>
<security>
<requestFiltering>
<filteringRules>
<!-- CVE-2026-111497: reject Brotli request bodies, logged as 404.19 -->
<filteringRule name="Block Brotli request bodies" scanUrl="false" scanQueryString="false" scanAllRaw="false">
<scanHeaders>
<add requestHeader="Content-Encoding" />
</scanHeaders>
<denyStrings>
<add string="br" />
</denyStrings>
</filteringRule>
</filteringRules>
</requestFiltering>
</security>
</system.webServer>
In the application: remove Brotli from existing configuration
Microsoft advises removing Brotli only where decompression is already switched on, and not adding new decompression features:
// only where AddRequestDecompression is already configured
builder.Services.AddRequestDecompression(options =>
{
// CVE-2026-111497: stop decoding Brotli request bodies until the runtime is patched
options.DecompressionProviders.Remove("br");
});
// HttpClient: was DecompressionMethods.All, drop Brotli until patched
var handler = new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
- Python: run
python3 -m pip uninstall -y Brotli brotlicffi, remove extras such ashttpx[brotli]and regenerate the lockfiles. requests, aiohttp and httpx then fall back to gzip and deflate. Other bundled Brotli copies are not affected by this. - Node.js: there is no workaround for responses to
fetch(), only the update. For inbound traffic the proxy block helps. - curl: remove
--compressedfrom scripts and.curlrc;curl -Vshows whether curl was built with Brotli. - Envoy and Istio: remove the Brotli decompressor filter until the proxies are updated.
- Limit outbound fetches: let services that load foreign URLs reach the internet only through an egress allow list. That cuts the risk for webhooks, previews and AI agents considerably.
4.Recognising attacks
An attack along the outbound path leaves no trace in your access logs, because the malicious response comes from the foreign server. Only logs of outbound connections and the service's crash reports help there. The traces at a glance:
| Trace | How you spot it | What it tells you |
|---|---|---|
Error code -17 | BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH in the logs of a patched service | someone sent a stream of exactly this shape |
Error code -9 | with an unpatched decoder the same stream fails later on the block length (per Bun issue 46352) | a hint; corrupt data triggers it too |
| Crashes while decompressing | segmentation faults, unexpected process exits, service restarts during decompression | no reliable proof on its own, but a reason to investigate |
| Suspicious requests | Content-Encoding: br on endpoints that never expect compressed bodies; IIS status 404.19 after the stopgap | attempt; only visible where the proxy or web server logs the header |
| Aftermath of code execution | foreign processes under the service account, new files, cron entries, systemd units, SSH keys | strong sign of a break-in |
If you suspect a compromise: isolate the service, secure logs and crash reports, rotate every secret the process could read and rebuild the system from a clean image. Because the flaw can also disclose memory, treat keys and tokens as exposed even without any sign of code execution. As a controller, you must report a personal data breach within 72 hours (Art. 33 GDPR); entities under NIS2 have their own reporting duties.
Frequently asked questions
What is Node2Shell?
Node2Shell, also called Broccoli2Shell, is the nickname for CVE-2026-111497, an out-of-bounds write in the Brotli decoder of Google Brotli 1.0.3 to 1.2.0. A crafted Brotli stream can corrupt the decoder's memory; the impact ranges from a crash and memory disclosure to code execution, which Imperva demonstrated in the lab on Node.js 24.22.0. Node.js fixed the flaw on 21 October 2026, and Google rates it 9.2 under CVSS 4.0.
Am I affected if I don't use Node.js?
Possibly, because the flaw is in the Brotli library itself. Also affected are the Python packages Brotli and brotlicffi, which take effect through requests, aiohttp and httpx, .NET 8, 9 and 10, Bun, Envoy with the Brotli filter, the libbrotli system library of Linux distributions and any program that bundles its own copy of the C code. What matters is whether your software decompresses Brotli data from an untrusted source.
Is updating the npm package brotli or Express enough?
No. Node.js ships the Brotli decoder inside the runtime, so only a fixed runtime helps: Node.js 22.23.4, 24.22.1 or 26.12.1. Express 5 and body-parser 2 are a path to the flaw, not the place where it is fixed. Afterwards every Node.js process has to restart and container images have to be rebuilt.
Are .NET Framework, IIS and ASP.NET Core affected?
Microsoft explicitly lists .NET Framework as not affected. Affected are .NET 8, 9 and 10 before 8.0.33, 9.0.22 and 10.0.14 on all platforms, which includes ASP.NET Core under IIS. Out of the box the flaw is not reachable there, though: it becomes dangerous with request decompression for br, with HttpClient automatic decompression that includes Brotli, or with direct use of BrotliStream and BrotliDecoder. Microsoft's advisory does not mention IIS itself.
Is Node2Shell being exploited?
As of 26 October 2026 no exploitation in the wild is known, and CISA has not added the flaw to its KEV catalogue. Imperva demonstrated code execution in the lab but did not publish the exploit; a public proof of concept only works against a deliberately weakened build. Microsoft rates exploitation as more likely.
What helps if I cannot patch right away?
For inbound requests, reject bodies with Content-Encoding br at the proxy or load balancer before the application decompresses them. In .NET, remove Brotli from existing request decompression and from AutomaticDecompression; in Python, temporarily uninstall Brotli and brotlicffi. There is no workaround for fetch() in Node.js; only the update helps there.
Sources
External sources, as of 26 October 2026 (open in a new tab):
- Node.js, October 2026 Security Releases
- NVD, CVE-2026-111497
- CVE.org, CVE-2026-111497
- GitHub Advisory Database, GHSA-9v8h-qqgm-4m35
- Google Brotli, release 1.2.1
- Google Brotli, commit 0ed4268
- Microsoft, security advisory CVE-2026-111497 for .NET
- .NET Blog, out-of-band updates of 25 October 2026
- Imperva, Node2Shell: From Brotli Data to Remote Code Execution
- Aikido, Python packages and further ecosystems
- OpenSourceMalware, Node2Shell affects Python
- Hadrian, What we know about the Node.js Brotli RCE
- broccoli2shell.com, overview of affected projects
- OSV, PYSEC-2026-4322 (Brotli)
- Ubuntu, USN-8956-1
- Debian Security Tracker, CVE-2026-111497
- Red Hat, CVE-2026-111497
- Amazon Linux, ALAS2023-2026-3334
- Vercel, Node.js security advisory
- SecurityWeek, patches across Microsoft, Node.js and Python
- Microsoft Learn, IIS request filtering rules
Take stock, patch, rebuild the images
You run Node.js, Python or .NET services and want to know where your systems decompress Brotli from untrusted sources? We check your servers and container images, roll out the fixes and narrow the paths that lead there, vendor-independent and GDPR-compliant, with data centres in Germany and Finland.
This article is for general information and does not replace a case-by-case assessment. Versions and commands refer to the stated date and need adapting to your environment. As of October 2026.