Tech16 min read

Brotli CVE-2026-111497 beyond Node.js, tested with Python, curl, and Git for Windows

IkesanContents

TL;DR

What happened Google’s Brotli library 1.0.3 through 1.2.0 has an out-of-bounds write in its decoder. Besides Node.js, anything that decodes with the Brotli C library is affected, including the Python Brotli and brotlicffi packages and curl builds with Brotli (when --compressed is used)
What to do Brotli 1.0.10, 1.1.1, or 1.2.1. In Python, Brotli>=1.2.1 and brotlicffi>=1.2.1.0. For Node.js, 22.23.4, 24.22.1, or 26.12.1, as in my previous post
What I checked Python with a Brotli package installed decoded the response even when it sent Accept-Encoding: identity. Git for Windows 2.56.0.windows.3 (released October 22) still bundles Brotli 1.2.0. I moved this PC’s Node.js from 22.23.2 to 22.23.4


In my previous post, Node.js v24.22.1 fixes four HIGH CVEs, tested against v24.22.0 and EOL v25.3.0, I covered the Brotli out-of-bounds write in Node.js (CVE-2026-111497).
There I treated it as one item in a Node.js security release, but the bug lives in Google’s Brotli library, which Node.js bundles.
The same library also ships inside Python packages and curl, so on October 23 Aikido gave it the name HTTP2Shell and warned that “many libraries and applications using HTTP” are affected (Aikido — Node2Shell Brotli vuln (CVE-2026-111497) evolves to HTTP2Shell).

To see how far it reaches outside Node.js, I checked Python, curl, and Git in turn on my Windows 11 PC.
I did not attack any external server. To find when the Brotli decoder runs, I used a local HTTP server that returns valid Brotli data.
Only for confirming the Node.js fix did I decode malformed data, taken from the test in the fix commit, on my own machine.

Test environment

ItemDetails
OSWindows 11 Home
Python3.14, urllib3 2.6.2, requests 2.32.5, unpatched brotli 1.2.0, pip-audit 2.10.1
curlWindows built-in 8.21.0 and the one bundled with Git for Windows, 8.22.0
GitGit for Windows 2.56.0.windows.2
Node.jsv22.23.2 (replaced with v22.23.4 partway through)
When checkedOctober 24, 2026, 13:00 to 14:00 JST

The names Node2Shell, HTTP2Shell, and Broccoli2Shell, and what the bug is

CVE-2026-111497 was first called Node2Shell because it was first disclosed by Node.js, and Aikido later called it HTTP2Shell.
There is also a page called Broccoli2Shell that collects the affected software, and all three names refer to the same CVE.
According to that page, the reporters were OpenAI’s Codex Security team, and the page’s author is not the person who found the bug.

Brotli is a compression format used for HTTP responses and other data. Compressed data is split into units called blocks, and the code table can be switched from block to block.
In the decoder, the function that reads these switches, DecodeBlockTypeAndLength(), only returned one of two values, success or failure.
Failure had two meanings, “the data is corrupt” and “more input has not arrived yet,” and the caller could not tell which one it got.
The faster code path also did not check the return value at all.

According to Aikido, if data declares only one block type, uses up that block, and then asks for a switch, decoding does not stop even though the switch cannot be valid.
The decoder carries on with the previous block’s state and later writes outside the allocated range.
The fix makes the function return an error code and stops decoding with a new error, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17).

The fix commit landed in the Brotli repository on February 27, 2026 (Detect and report invalid stream earlier.).
The commit message only says it detects and reports invalid streams earlier and does not call it a security fix.
The fix first shipped in Brotli 1.0.10, 1.1.1, and 1.2.1 on October 21 (google/brotli Releases).

Vulnerability severity is often expressed with CVSS (Common Vulnerability Scoring System), a shared 0-to-10 scoring scheme.
Google, which assigned the CVE, rates it 9.2 under CVSS 4.0 (Critical, the most severe band), and Node.js rates it HIGH on its own scale (OSV — CVE-2026-111497).
The CVE description says a remote, unauthenticated attacker can corrupt memory, leak memory contents, cause a denial of service, or possibly execute arbitrary code (run code the attacker supplies).

As an example that went as far as code execution, Imperva used the official Node.js 24.22.0 (Linux arm64) container image, sent crafted Brotli data to an app built with Express 5 and body-parser 2, and made it run code the app does not contain (Imperva — Node2Shell: From Brotli Data to Remote Code Execution).
It did not work every time and needed several attempts, and Imperva has not published the exploit.
According to the Broccoli2Shell page, no in-the-wild attacks had been reported as of October 22.

Software affected by the Brotli bug and fixed versions

Software that carries its own copy of the Brotli decoder stays vulnerable until it ships its own update.
Node.js from nodejs.org also bundles Brotli inside the binary, so updating the OS libbrotli does not fix it (Node.js built by a Linux distribution can use the OS Brotli, depending on its build settings).
I found fixed versions for six pieces of software in the sources below.

SoftwareAffected versionsFixed versionsSource
Google Brotli (C library)1.0.3 to 1.2.01.0.10, 1.1.1, 1.2.1Brotli Releases
Node.js22.x, 24.x, 26.x before the fix22.23.4, 24.22.1, 26.12.1Previous post
Python BrotliBefore 1.2.11.2.1GHSA-9v8h-qqgm-4m35
Python brotlicffiBefore 1.2.1.01.2.1.0GHSA-9v8h-qqgm-4m35
BunVersions before the fix1.4.4Aikido
EnvoyEach release line before the fix1.40.0, 1.39.3, 1.38.6, 1.37.8 (1.36.12 also has the fix, but the 1.36 line is out of support)Aikido

End-of-life Node.js lines such as v25, v23, and v20 get no fix.

When Aikido says “Fixes not yet available for all affected services,” it means software that still carries a Brotli copy without an update.
The Broccoli2Shell page lists, as of October 24, things that still carry an unpatched copy, including the .NET runtime, the PHP brotli extension, the Ruby brotli gem, DuckDB, ClickHouse, Apache Arrow, and 7-Zip forks.
On the other hand, Aikido says Apache’s standard mod_brotli and nginx do not decode Brotli in their default configuration (they only compress what they send).

Replacing Node.js on this PC with 22.23.4

Node.js on this PC was v22.23.2, a vulnerable version.
I don’t use a package manager such as winget or nvm for it. I extracted the Windows zip from nodejs.org into a folder and put that folder on PATH.
Packages installed globally with npm -g (Codex CLI, Vercel CLI, and others) also live in that folder’s node_modules.

Because of that bundling, replacing node.exe with the fixed build resolves the bug on Windows too.
The zip only contains node.exe, the bundled npm and corepack (the tool that launches pnpm and Yarn), and their launcher scripts, so I could overwrite it while keeping the global packages.

I downloaded the fixed zip, checked its hash against SHASUMS256.txt, and extracted it. I ran the commands in Git Bash.

curl -LO https://nodejs.org/dist/v22.23.4/node-v22.23.4-win-x64.zip
curl -L https://nodejs.org/dist/v22.23.4/SHASUMS256.txt | grep " node-v22.23.4-win-x64.zip$" | sha256sum -c
unzip -q node-v22.23.4-win-x64.zip

A running node.exe cannot be overwritten, so I used tasklist to make sure no Node.js process was running before I started.
To be able to roll back, I moved the current node.exe, the launcher scripts, and the npm and corepack folders under node_modules somewhere else, and then deleted the npm and corepack folders.
Overwriting without deleting them would leave behind files that the new version no longer has, so I deleted them first and then copied the whole extracted folder over.

Before and after the swap, as in my previous post, I passed the malformed Brotli data from the test in the Node.js fix commit (data that switches blocks even though it has only one block type) to zlib.brotliDecompressSync() (src: expose BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH).
The same commit also added the constant zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH for the new error, so I checked its value too.

ItemBeforeAfter
node -vv22.23.2v22.23.4
process.versions.brotli1.1.01.1.0
zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCHundefined-17
Decoding the malformed Brotli dataERR__ERROR_FORMAT_BLOCK_LENGTH_1ERR__ERROR_FORMAT_BLOCK_SWITCH
npm10.9.810.9.9
corepack0.34.60.36.0

After the swap, the malformed data stops with ERR__ERROR_FORMAT_BLOCK_SWITCH, the error added by the fix, so the fix is in.
However, process.versions.brotli still shows 1.1.0.
That is because Node.js did not move its bundled Brotli to 1.2.1. It applied only Brotli’s fix commit on top of the 1.1.0 source (CHANGELOG_V22).
The “Brotli 1.0.3 to 1.2.0 is affected” range describes upstream Google Brotli used as is, and does not apply to the copy bundled in Node.js.
To tell whether Node.js is fixed, check whether node -v is a fixed version or whether the constant added by the fix exists, not the Brotli version string.

node -p "process.version + ' ' + require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH"

On a vulnerable version the constant is undefined, and on a fixed version it is -17.
After the swap, pnpm (through corepack), Codex CLI, and Vercel CLI all still started normally.

When Python decodes Brotli responses

urllib3, a widely used Python HTTP client (a library for making HTTP requests), is also what requests uses under the hood.
urllib3 decodes Brotli responses only when the Brotli or brotlicffi package is installed in the environment, in which case it loads that package.
According to Aikido, httpx (when installed with the brotli extra), aiohttp, and Scrapy behave the same way: even if the app never runs import brotli, having the package installed is enough for decoding to happen.

I created two Python 3.14 virtual environments and installed the unpatched brotli==1.2.0 in only one of them.
The local HTTP server compresses hello brotli with Brotli and returns it with Content-Encoding: br, whatever the request says.
Accept-Encoding is the header that tells the server which compression formats the client can accept, and identity means “please don’t compress.”
decode_content=False is an option that tells urllib3 not to decode the response.
Both environments used urllib3 2.6.2 and requests 2.32.5, and I recorded the Accept-Encoding that was sent and the body that came back.

How the request was madeWith brotli 1.2.0Without brotli
requests defaultsSent gzip, deflate, br, zstd and decodedSent gzip, deflate, zstd and did not decode
requests with Accept-Encoding: identityDecodedDid not decode
urllib3 with Accept-Encoding: identityDecodedDid not decode
urllib3 with decode_content=FalseDid not decodeDid not decode

Even when it sent identity, urllib3 in the environment with a Brotli package decoded it based on the response header whenever the server returned Content-Encoding: br.
In my previous post I confirmed that Node.js fetch() also decodes based on the response header regardless of what it sent, so with both urllib3 and fetch(), a malicious server can get its response decoded whatever the request header says.

Aikido says code that fetches data from external servers is the entry point, and lists webhook destinations, URL previews, and crawlers (programs that automatically visit and download web pages), where users or outsiders choose the URL being fetched.

You can check whether the packages are installed with pip show Brotli brotlicffi, and trace which package pulls in Brotli as a dependency with pipdeptree -r -p Brotli.

pip show Brotli brotlicffi
pip install -U "Brotli>=1.2.1"
pip install -U "brotlicffi>=1.2.1.0"

If you use a lock file (a file that pins dependency versions) with Poetry, uv, or Pipenv, update the lock file and then rebuild container images and the like.

Detection with pip-audit

Both Aikido and OpenSourceMalware, which first analyzed the impact on Python, wrote on October 23 that the PyPI packages were not yet registered in vulnerability databases, so dependency scanners would not flag them.
Since then, the GitHub Advisory Database (GHSA) entry has added the PyPI packages brotli and brotlicffi, and the GitHub API shows the review time as 19:08 UTC on October 23 (GHSA-9v8h-qqgm-4m35).
The Python vulnerability database also gained PYSEC-2026-4322 (Brotli) and PYSEC-2026-4323 (brotlicffi).

When I ran pip-audit 2.10.1 in the virtual environment with brotli 1.2.0 at 13:12 JST on October 24, 2026, it flagged brotli.
Because different vulnerability sources can register entries at different times, I tried both the default source (PyPI) and OSV via -s osv, and both flagged it.

Name     Version ID                  Fix Versions
-------- ------- ------------------- ------------
brotli   1.2.0   PYSEC-2026-4322     1.2.1
brotli   1.2.0   GHSA-9v8h-qqgm-4m35 1.2.1

The “scanners don’t detect it” situation that Aikido and OpenSourceMalware described was already resolved for pip-audit at this point.

The Brotli bundled with curl and Git for Windows

curl can decode Brotli responses if it was built with Brotli.
This PC has two curls, the one that ships with Windows (C:\Windows\System32\curl.exe) and the one bundled with Git for Windows, and their curl -V output differs.
The curl command only decodes when you pass --compressed. Without it, both curl builds printed the compressed bytes as is.

curlBrotli in curl -VOptionsResult
Windows built-in 8.21.0None--compressed onlyError 61 (Unrecognized content encoding type)
Git for Windows bundled 8.22.0brotli/1.2.0--compressed onlyDecoded
Git for Windows bundled 8.22.0brotli/1.2.0--compressed and -H "Accept-Encoding: identity"Decoded

In the third row, curl really did send identity, but because the response said Content-Encoding: br, it decoded it. As with Python, the request header does not stop it.

After curl, I looked at Git’s own traffic.
Git uses this curl library (libcurl) for HTTPS, and unlike the curl command, it tells the server it accepts br without any option like --compressed.
Running git ls-remote with GIT_TRACE_CURL=1 against the local server from the Python test (which always returns Brotli-compressed data), Git sent Accept-Encoding: deflate, gzip, br, zstd and received a response with Content-Encoding: br.
The server is not a Git repository, so Git then stopped with not valid: is this a git repository?.
What I observed directly is Git advertising br and receiving a br response.
That the response then gets decoded follows from Git’s source. Git’s http.c sets CURLOPT_ENCODING to an empty string (git/http.c), and according to the libcurl documentation, that setting lists every built-in encoding in Accept-Encoding and enables decoding when the response has a Content-Encoding header (CURLOPT_ACCEPT_ENCODING).

The Git for Windows on this PC is 2.56.0.windows.2, and its bundled package list (C:\Program Files\Git\etc\package-versions.txt) says mingw-w64-ucrt-x86_64-brotli 1.2.0-1.
The latest Git for Windows is 2.56.0.windows.3, released on October 22, and that was still the case at 14:00 JST on October 24.
I extracted MinGit, the minimal no-install build from that release, and checked the same list. curl had moved to 8.23.0, but Brotli was still 1.2.0-1.
The release notes don’t mention Brotli or CVE-2026-111497 either (Git for Windows Releases).

Since even the latest release had the old Brotli, I looked at MSYS2, where Git for Windows gets its packages. A commit moving Brotli to 1.2.1 went in on October 22, and mingw-w64-ucrt-x86_64-brotli was at 1.2.1-1 (MSYS2 Packages — mingw-w64-ucrt-x86_64-brotli).
Until Git for Windows ships a release that picks this up, connecting to an untrusted HTTPS server with git clone or git fetch means its responses are decoded by the unpatched Brotli.

Express 5 decodes request bodies on the server side

Request bodies that a server receives can also be decoded if they carry Content-Encoding: br.
In that case the attacker only has to send a request, with no login needed.

A server built on the Node.js http module does not decode the body on its own.
In Express, express.json() and express.raw(), which read the body, use body-parser (a library that parses request bodies as JSON and other formats) internally.
The body-parser README says every parser supports decoding gzip, br, deflate, and zstd, and that the inflate option, which controls whether decoding happens, defaults to true (expressjs/body-parser).
Imperva’s demonstration mentioned at the start also used Express 5 with body-parser 2.

Upgrading Node.js to a fixed version closes this path too.
Until you can upgrade, if no client needs to send compressed bodies, you can turn decoding off with something like express.json({ inflate: false }).
With this setting, compressed bodies are rejected with an error instead of being decoded.

Mitigations until you can install a fixed version

As mitigations until a fixed version can be installed, Aikido and OpenSourceMalware suggest refusing Brotli data or turning off decoding altogether.

WhereMitigationNote
Servers that receive requestsReject requests whose Content-Encoding contains br at a reverse proxy (a server in front of the app that relays requests) or in middlewareAlso catch different letter cases and lists such as gzip, br
Clients that fetch from outsideDisable Brotli decoding, or check the response’s Content-Encoding and reject br before decoding startsA server can still send br after you remove it from Accept-Encoding
Python clientsUninstall Brotli and brotlicffiYou can no longer talk to servers that only return Brotli. Also check for dependencies such as httpx[brotli] that would reinstall them

In the Python environment without brotli, a Content-Encoding: br response came back still compressed, without being decoded.