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.

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:

Three paths lead into the Brotli decoder of versions 1.0.3 to 1.2.0: inbound requests with Content-Encoding br, responses from foreign URLs and your own code decompressing files or messages. An invalid block switch counts as a need for more input, and the decoder writes past an eight-byte buffer. Results: crash, memory disclosure and, in the lab, code execution.
Figure 1: How untrusted data reaches the decoder and what happens there.

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:

EnvironmentAffectedFixed inNote
Node.js 22 · 24 · 26yes, decoder in the runtime22.23.4 · 24.22.1 · 26.12.1Node.js 20, 21, 23 and 25 get no fix. The Ubuntu and Debian Node.js packages use the system library.
Bunyes1.4.4via fetch(), node:zlib and DecompressionStream
Python Brotli · brotlicffiyes1.2.1 · 1.2.1.0takes effect through requests/urllib3, aiohttp, httpx[brotli], Scrapy; often pulled in as an indirect dependency
.NET 8 · 9 · 10yes, not reachable by default8.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 · PowerShellyesupdates of 25 OctoberVisual Studio 2019, 2022, 2026; PowerShell 7.4, 7.5, 7.6
System library libbrotliyessee next tableprograms that load it need a restart after the update
Envoy · Istiowith the Brotli decompressor filterEnvoy 1.40.0, 1.39.3, 1.38.6, 1.37.8; Istio 1.29.9, 1.30.6, 1.31.2remove the filter from your EnvoyFilters until updated
Javaonly JNI wrappers around the C codebrotli4j 1.24.0pure Java ports not affected
PHP · Rubyextension or gemPHP extension brotli 0.21.1 · gem 0.9.1copies bundled in other projects stay affected until updated
Go · RustGo port only crashes, Rust catches the caseandybalholm/brotli since 23 Octobercgo bindings around the C code are affected
Apache mod_brotlinonot neededcompresses only, never decompresses
Chromium · Firefoxfixedper broccoli2shell.comupdate browsers as usual
DuckDB, Apache Arrow, 7-Zip-zstd, p7zip, EDK IIyesstill openper 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:

SourcePackageFixed inNote
Google Brotli (upstream)1.0 · 1.1 · 1.21.0.10 · 1.1.1 · 1.2.1fix from commit 0ed4268
Ubuntu 26.10brotli, libbrotli1, python3-brotli1.2.0-4ubuntu0.1USN-8956-1
Ubuntu 26.04 LTSas above1.2.0-3ubuntu0.1USN-8956-1
Ubuntu 24.04 LTSas above1.1.0-2ubuntu0.1USN-8956-1
Ubuntu 22.04 LTSas above1.0.9-2ubuntu0.1USN-8956-1
Debian 13 trixiebrotli1.1.0-2+deb13u1DSA-6582-1
Debian 12 bookwormbrotli1.0.9-2+deb12u1DLA-4855-1 (Debian LTS)
Alpinebrotli1.2.1-r0 (edge, 3.23, 3.24) · 1.1.1-r0 (3.21, 3.22)per Aikido
Amazon Linux 2023brotli1.1.0-1.amzn2023.0.2ALAS2023-2026-3334
RHEL 8 · 9 · 10brotlipartlyRed Hat: "Important", CVSS 3.1 8.1; bulletin RHSB-2026-015
Fedora 43 to 45 · SUSEbrotlistill openper Aikido on 26 October
Node.js (official builds)22 · 24 · 2622.23.4 · 24.22.1 · 26.12.1official Docker images too, since 21 October
Python (PyPI)Brotli · brotlicffi1.2.1 · 1.2.1.0PYSEC-2026-4322, PYSEC-2026-4323
.NET runtime8 · 9 · 108.0.33 · 9.0.22 · 10.0.14SDKs 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.

BashNode.js: version against the fixed release
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 '...'
BashPython: Brotli and brotlicffi
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:

Bashlibbrotli1 and old code in memory (Debian 12)
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:

PowerShell.NET runtime on Windows
$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" } }
Bash.NET runtime on Linux
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:

BashWhere does your code decompress Brotli?
# .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.

BashUbuntu and Debian
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
BashPython packages (only those step 1 reports as installed)
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

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

Configurationnginx, HAProxy
# 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 }
XMLIIS: request filtering rule in web.config (IIS 7.5 and later)
<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:

C#ASP.NET Core and HttpClient
// 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
};

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:

TraceHow you spot itWhat it tells you
Error code -17BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH in the logs of a patched servicesomeone sent a stream of exactly this shape
Error code -9with 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 decompressingsegmentation faults, unexpected process exits, service restarts during decompressionno reliable proof on its own, but a reason to investigate
Suspicious requestsContent-Encoding: br on endpoints that never expect compressed bodies; IIS status 404.19 after the stopgapattempt; only visible where the proxy or web server logs the header
Aftermath of code executionforeign processes under the service account, new files, cron entries, systemd units, SSH keysstrong 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):

IT security with Nokkela

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.

Enquiry More on nokkela.network

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.