The Brotli decoder bug that Node.js patched on October 21 lives in Google's C library, and that library is compiled into Python packages, Bun, Envoy, Istio, libcurl, .NET, and a dozen file parsers. Four days after the Node release, the bug had its third name, HTTP2Shell, and most of the bundled copies were still unpatched.
The CVE is CVE-2026-111497. Google scored it 9.2 on CVSS 4.0. GitHub's advisory scores it 9.8. Node.js calls it High. Pick whichever number you like, the mechanics are the same: a crafted Brotli stream writes past the end of an eight-byte internal buffer in the decoder, and the process that decompressed it can crash or, in the worst case, run attacker code. Memory disclosure sits in between.
What the decoder got wrong
The root cause is an unchecked return value. The function DecodeBlockTypeAndLength() in decode.c returned a bare true or false, so a malformed block switch and a request for more input looked identical to the caller. When the streaming decoder hit an invalid block switch, it reported "need more input" instead of "format error" and kept appending bytes to a buffer sized for eight. OpenSourceMalware fed a 23 MB crafted stream to Brotli 1.2.0 built with AddressSanitizer. The decoder read the whole thing and asked for more. Version 1.2.1 rejected the same stream with error -17 every time.
Every Brotli release from 1.0.3 through 1.2.0 has the flaw, which covers releases going back to 2018. The fix is upstream commit 0ed4268, titled "Detect and report invalid stream earlier," which adds a new error code, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. Strobes and Aikido both date that commit to February 2026, and its message carries no security label. Google tagged releases 1.0.10, 1.1.1 and 1.2.1 on October 21 at about 19:26 UTC, nine hours after Node.js shipped its own patch. Before that, anyone linking a released Brotli had no fixed version to pin.
The Node.js advisory credits OpenAI Codex Security, Khai Tran and Sylvie Mayer, with the report. Aikido's October 24 update says OpenAI found the flaw in February. If both are right, a fix for a 9.x decoder bug sat on the main branch for eight months before any downstream project shipped it.
Who decodes Brotli they don't control
The exposure test is one sentence: your process decodes Brotli data that came from someone else. Apache's mod_brotli and Nginx's brotli module compress responses. They don't decompress request bodies, so a stock web server in front of your app is fine. The problem is behind it.
Inbound, Express 5.0.0 through 5.3.0 ships body-parser 2.0.0 through 2.3.0, and body-parser 2 sets inflate to true by default. Send a POST with Content-Encoding: br and the body goes into the decoder before your route code runs. Express 4 with body-parser 1.x returns a 415 for Brotli and is not exposed on that path.
Outbound, Node's built-in fetch() decodes br responses on its own. So does libcurl when you ask it to decode compressed responses. Python's requests, urllib3, aiohttp and httpx all use Brotli if the Brotli or brotlicffi package happens to be installed, with no import in your code. Aikido puts the Brotli package at 82.8 million PyPI downloads a month. A webhook receiver, a link previewer, a scraper, a feed reader. Each one talks to a server that chooses the content encoding.
Then the long tail. The same C code is bundled into DuckDB, ClickHouse, Apache Arrow, libjxl, MuPDF, the 7-Zip forks, the PHP brotli extension, the Ruby brotli gem, and the .NET runtime's BrotliDecoder and HttpClient. Strobes' October 24 tracker listed all of those as unpatched, with Chromium, Firefox, ngx_brotli, Brotli4j and Ghostscript already fixed. Parquet files compressed with Brotli and JPEG XL images are Brotli streams too.
Proof of execution, with caveats
Imperva's Red Team published on October 22. They ran the official Node.js 24.22.0 Linux arm64 image in fresh containers, with Express 5 and body-parser 2 pinned at several versions, plus a test of fetch() reading a compressed response. The test app had no code that writes files. After it received the crafted stream, a marker file appeared on disk. No debugger, no instrumented build. The same inputs against 24.22.1 produced a rejected request and a server that kept running.
Imperva says execution took multiple attempts and did not land every time, and they did not test every build or application. The public proof of concept repos that appeared October 21 and 22 crash stock builds and only execute code on deliberately weakened ones. CISA's KEV catalog does not list the CVE, and its SSVC data on October 23 marked exploitation as none. GovCERT.CZ issued a national warning anyway.
So the status on October 25 is this. Memory corruption is confirmed everywhere the library is built. Code execution is demonstrated on one official Node build by one team. Nothing has been seen in the wild. That last fact has a short shelf life once a reliable exploit exists for a bug reachable from any HTTP body.
Patch list and the gaps in it
- Node.js: 22.23.4, 24.22.1 or 26.12.1. Lines 25, 23, 20 and older get nothing. Node 20 went end of life on April 30 this year, and HeroDevs found the vulnerable code in the final tag of every line back to 10.x.
- Bun: 1.4.4.
- Python: Brotli 1.2.1 (on PyPI October 22) and brotlicffi 1.2.1.0 (October 23). requests and urllib3 need nothing of their own.
- Envoy: 1.40.0, 1.39.3, 1.38.6 or 1.37.8. Istio: 1.29.9, 1.30.6 or 1.31.2.
- System libbrotli: 1.0.10, 1.1.1 or 1.2.1. Debian bookworm had a fix by October 23. Trixie and unstable did not.
Two traps. Debian builds Node.js against the system libbrotli, so on Debian the fix comes from the brotli package and the Node version string tells you nothing. Everywhere else, Node vendors its own copy under deps/brotli, so the npm package called brotli is unrelated and updating it does nothing. And pip-audit, Dependabot and Safety had not mapped the CVE to the Python packages as of this weekend. A clean scan is not a clean system.
A patched library on disk does not patch a running process. Restart everything that loaded the old one and rebuild every image that baked it in. If you can't upgrade yet, Vercel and Imperva both deployed WAF rules that reject Content-Encoding: br on request bodies, and Express 5 users can pass inflate: false to express.json(), which also rejects gzip with a 415. Neither blocks the outbound path. For that, uninstall Brotli and brotlicffi where they're optional, and run the services that fetch URLs as an unprivileged user with no secrets in the environment. Stripping br from your Accept-Encoding header does nothing. The server picks the encoding, and a hostile one will pick br.
The library you forgot you shipped
I've run hosting infrastructure long enough to know that the compression library is the last thing anyone inventories. It arrives as a transitive dependency, a runtime vendor directory, or a distro package nobody chose. The 2020 Brotli CVE, an integer overflow in this same decoder, got the same short release note and the same slow rollout through embedders.
Four names in four days means the disclosure failed at step one. Node.js shipped on the morning of October 21 as if this were a Node bug. Upstream Brotli released that evening. Python packages followed on the 22nd and 23rd, Envoy and Istio behind them, and on the 24th a dozen projects with bundled copies still had nothing. A single advisory from Google on day one, naming the library and every known embedder, would have saved most of that week. Instead each downstream learned about it from the one before.
My bet is that the first exploitation in the wild lands on a Python scraper or a Node link previewer, not an Express API. Those services fetch hostile URLs all day, and nobody puts a WAF on outbound traffic. Check your fetchers first.