Back to Blog

Your inventory has no line for Brotli

Your inventory has no line for Brotli

On 24 October somebody outside Microsoft read the decoder bundled in .NET 9.0.21 and 10.0.13, saw that it still carried the pre-fix behaviour, and opened an issue titled with a question: "Does CVE-2026-111497 affect System.IO.Compression.Brotli in .NET 9 and 10?". Microsoft answered the next day and by the 25th there were patched builds. An outsider had to read the runtime's own code in order to ask the vendor whether its product was broken, and was right.

The flaw, with the number up front

CVE-2026-111497. An out-of-bounds write in Google's Brotli decoder, the one that unpacks the Content-Encoding: br travelling across half the web. Google, as the assigning authority, scored it 9.2 out of 10 on CVSS 4.0, with the vector AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H: over the network, no privileges, no user interaction. The technical classification is CWE-787 — out-of-bounds write — rooted in CWE-252: a return value nobody checked.

The Node.js advisory puts it like this: "an invalid block switch was reported as a temporary need for more input instead of a permanent format error, so the streaming decoder kept appending input bytes to an eight-byte internal buffer". Eight bytes. The decoder believes it is short of material, asks for more, and carries on writing past the end. From there follow, in this order: memory corruption, disclosure of process memory, a crash and — under the right heap conditions — code execution.

It was found by OpenAI's Codex security team — Khai Tran and Sylvie Mayer — and fixed in Node by Matteo Collina and Antoine du Hamel. Upstream, the Brotli project corrected it in 1.0.10, 1.1.1 and 1.2.1; everything between 1.0.3 and 1.2.0 was affected. That range covers three major branches and everything in between.

If you want the root cause without intermediaries, the upstream fix commit is called "Detect and report invalid stream earlier" and what it does is telling: it adds an error code that did not exist, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, and changes three block-switch functions — literal, command and distance — so they return error codes instead of a plain yes or no. That is the whole story in one line of diff: "malformed block switch" had no way of being said, so the decoder said the only thing it knew how to say, which was "give me more data".

And now the figure that reorders everything else. That commit is dated 27 February 2026. Eight months before the flaw was made public, the fix was already in Brotli's main branch, in plain sight. What was published on 21 October was not the fix: it was the advisory. During those eight months, whether you had the flaw or not depended solely on when the project that serves it to you last updated its dependency.

Five names in four days

On 21 October, when Node shipped the fix, the flaw was called Node2Shell. On the 23rd, Aikido first named it Python2Shell and renamed it HTTP2Shell the same day, on seeing it was not a language problem but a problem for anyone decompressing an HTTP response. And on the 25th two analyses were published the same day under two further names: JFrog called it Brotli2Shell, subtitled with no subtlety whatsoever "beyond Node(2Shell) and HTTP(2Shell)", and Wiz called it Broccoli2Shell, picking up researcher Juliano Rizzo's suggestion.

The justification for the last one is the cleanest sentence written about any of this: the flaw "was first called Node2Shell, but the bug is in Brotli itself and reaches well beyond Node.js". Five names in four days, and what drives each one is always the same thing: somebody discovering that the blast radius was wider than their mental model. First it was framed as a language, then a product, then a protocol. The last two names, published on the same day by two different teams, finally name the library.

Your inventory is written in the first language. You have a line for Node, one for .NET, one for your data platform. None for Brotli. We keep our clients' network inventory in NetBox, as a single source of truth, and it is worth saying what that does not solve: NetBox knows which machine is in which cabinet, with which address and on which circuit. It does not know which libraries are inside the binaries running on top. No infrastructure inventory tool knows that, because it is not the question they were designed for.

The word that repeats across every advisory: "bundled"

The word shows up in all four advisories. Node talks about the "Brotli decoder bundled with Node.js". The Apache Arrow issue is titled, literally, "[C++] Bundled Brotli 1.0.9 is affected by CVE-2026-111497". In the .NET repository, whoever opened the question refers to the "vendored Brotli decoder in .NET 9.0.21 and 10.0.13". And ClickHouse goes a step further: it does not bundle Google's Brotli, it bundles a fork of its own — carrying a patch for the loongarch64 architecture — and what it did on 25 October was update that fork.

Look at Arrow's number: 1.0.9. It is not the last of the affected branch — that is 1.2.0 — nor the one before it: it sits several versions behind, bundled inside the project itself, while the library went on its way outside. Nobody installed it, nobody chose it and nobody is going to see it with an apt. Debian's security tracker, in fact, lists exactly three source packages for this CVE: brotli, nodejs and python-brotlicffi. Your package manager will tell you about those three and it will be telling the truth. It will also tell you nothing about the copy inside Arrow, the one inside .NET and ClickHouse's fork.

That is why JFrog's researchers — Aviv Engelberg, Eddy Tsalolikhin and Ofri Ouzan — did not just publish an analysis: they published a scanner, and justified it by saying it finds Brotli decoding paths that version-only scanners miss. It looks for JavaScript calls (fetch, Express, body-parser), Python clients (Requests, HTTPX, aiohttp, urllib3), decoder references inside ELF binaries and command-line usage. In other words: they stopped asking "which version do you have installed?" and started asking "which of your code decompresses this?". It is not the first time that distinction decides the outcome; we wrote about it when we explained why your scanner says zero criticals and the exploit gets root.

You are the one making the request (which is why your perimeter is not looking)

The reflex is to think about the server: somebody sends me a body compressed with br and blows up my process. That happens. Envoy and Istio — the proxies many have in front of everything — show up on the fixed lists, though with a wrinkle worth looking at further down. But the wider path runs the other way.

The PyPI packages Brotli and brotlicffi ship the vulnerable decoder, and requests, urllib3, aiohttp and httpx use either one automatically to decode Content-Encoding: br responses. Automatically means: without you writing a line, without it appearing in your code, without you having decided it. Your background worker calls a third-party API, the third party answers compressed, and the vulnerable decompressor runs inside your process, behind your firewall.

The perimeter does not cover you here because the perimeter is drawn around a different question: who is coming in. Nobody is coming in. You are going out, and coming back with something in your hands. It is the modern version of a resilience classic. The part that breaks you is the one you took for granted. A compression decoder failing is inevitable at ecosystem scale; whether that becomes an outage depends on whether you can answer, within hours, where your copy is and who updates it.

"Is it patched?" has no single answer: it has eight clocks

This is the table that matters, with the state as of 26 October 2026. What changes from row to row is three things: the version, what the attacker gets and whether there is anything to install.

Where What the flaw achieves there Clock as of 26 Oct 2026
Node.js Remote code execution via node:zlib Fixed on 21 Oct in 22.23.4, 24.22.1 and 26.12.1. Branches 18, 20, 23 and 25 are end of life: there will be no patch
Bun Same decoder, same reach Affected 1.0.21 to 1.4.3; fixed in 1.4.4
Python Triggers by itself, decoding br responses from requests, urllib3, aiohttp or httpx Brotli fixed in 1.2.1; brotlicffi in 1.2.1.0
Envoy / Istio Only if you enabled Brotli decompression, which is not on by default: then it decompresses before anyone else Envoy: 1.36.12, 1.37.8, 1.38.6, 1.39.3 and 1.40.0. Istio: 1.29.9, 1.30.6 and 1.31.2, and only if you have brotli decompression enabled
.NET Process crash via a native segmentation fault Updates rolling from 25 Oct: 8.0.33, 9.0.22, 10.0.14 and 11.0.0-rc.3
ClickHouse Denial of service decompressing HTTP requests: your query is cut off Merged on 25 Oct (PR #127419), with backports recorded to 26.10.3.6, 26.9.26.18, 26.8.35.17 and 26.3.61.13
Apache Arrow Infinite CPU loop decompressing Parquet Issue open (#55166, from 23 Oct). Still bundling Brotli 1.0.9
FFmpeg / libjxl Memory corruption decoding a crafted JPEG XL JFrog got a segfault at 0x414141414141 freeing a pointer derived from the payload

The two rows at the extremes are the ones that matter. In Node, on 21 October there was a binary to install; five days later, Apache Arrow still has an open issue and a Brotli 1.0.9 inside it. If your data platform reads Parquet through Arrow, "update" is not a plan: there is nothing to install yet. What is left there is operational rather than patching: refuse br on the inputs you control and stop advertising Accept-Encoding: br on the calls you make, until the binary exists. It is awkward and it is slower, and it is the difference between having a plan and having a sentence.

And there is a ninth clock, which is not in the table because it belongs to nobody in particular. Envoy's fixed versions shipped between 6 and 13 October; Istio's on the 15th and 16th. Before the CVE was public. They did not react fast: they had updated their Brotli dependency at some point after February and found themselves fixed without knowing it. That is the other half of the answer to "does this affect us?", and it is the half you do not control: how often your suppliers update what they carry inside.

The 9.2 and the "none": two columns that do not resolve

Column one. A critical 9.2 assigned by Google, network, no privileges, no interaction. Wiz published on 25 October that 91% of the cloud environments it sees have vulnerable resources and 82% have publicly exposed Node.js instances. And Imperva ran attacker-supplied code inside applications using the official Node.js 24.22.0 Linux arm64 image, unmodified, with Express 5.0.0–5.3.0 and body-parser 2.0.0–2.3.0; the same inputs were rejected by 24.22.1.

Column two. CISA's SSVC entry of 23 October records exploitation as "none" and automatable as "no". Red Hat scores it 8.1 on CVSS 3.1 rather than 9.2 and writes that "there is no public, reliable remote code execution proof-of-concept". Node.js itself classified it as High, not Critical. And Imperva's exploit is not published: they themselves note that execution took several attempts and that they did not test every build.

Both columns are true and they do not cancel each other out. What decides the outcome is a figure that appears in neither: how long it takes you to go from "a patch exists" to "it is installed". If that number is measured in days, column two gives you plenty of room. If it is measured in months, column two is irrelevant, because by the time a real attack shows up you will still be exactly where you are now. We told the story of a case where that distance had been measured: the Redis patch had been out for 164 days when they started getting in.

The five-question census (you can do it this afternoon)

Five questions with concrete answers. We ask them in this order when we walk into a platform we do not know, and if one of them leaves everyone in the room blank, that is the one to work on. Start here:

# 1. Which copies of Brotli you have, without asking the package manager node -v # compare against 22.23.4 / 24.22.1 / 26.12.1 # Do NOT use node -p "process.versions.brotli" to decide: Node patches by cherry-pick # WITHOUT bumping that number. An already-patched 24.22.1 still reports 1.2.0. pip list 2>/dev/null | grep -i brotli # Python's: 1.2.1 / brotlicffi 1.2.1.0 dotnet --list-runtimes # compare against 8.0.33 / 9.0.22 / 10.0.14 apt list --installed 2>/dev/null | grep -i brotli # ONLY the system one # 2. And now the ones nobody declares strings /ruta/al/binario | grep -i "brotli" # needs binutils; gives no version find / -name "*brotli*" -not -path "*/proc/*" 2>/dev/null # CAREFUL: find only sees files. Statically linked copies — Arrow, .NET, # ClickHouse: precisely this article's case — have no file of their own.
  1. Which of your things decompress Brotli without you asking? Not "what do you have installed": what runs the decoder. Every worker calling an external API, every proxy, every process reading Parquet. If the list comes out short on the first pass, you have not looked inside the containers.
  2. For each copy: is it the system's or is it bundled? That distinction decides who fixes it. The system one is fixed by your distribution. The bundled one is fixed by the project that put it there, at its own pace, and you find out by reading its repository.
  3. For the ones with no fixed release: what is the operational mitigation, in writing? Reject Content-Encoding: br on the inputs you control, stop advertising Accept-Encoding: br on outbound calls. With a review date, because it is temporary and temporary things without a date become permanent.
  4. Who looks at this in two weeks? An advisory that in five days had four names and eight different patch states does not get closed with a Friday email. It needs an owner and a review date. If the answer is "somebody already looked at it", there is no answer.
  5. What do you do with what is out of support? Node 18, 20, 23 and 25 are never going to get this patch. That no longer fits in a security ticket. It is an architecture decision, and somebody has to make it with a date attached. Every CVE from now on will ask you the same question again.

When not to run

And the opposite case. If nothing of yours decompresses Brotli that came from outside, this is a routine update and not a Saturday. An internal application that only talks to itself, on a closed network, with no outbound calls to third parties, is not in the front row here no matter how red its Node version looks in a report. Put it in the normal cycle and get on with your week.

The opposite risk to panic is uniform treatment, and it does more damage than it looks: treating every advisory of the month with the same urgency is exactly how you spend the credit you will need on the day one of them really is being exploited. The severity scale is not set by the CVSS, it is set by the answer to question one of the census.

We have been running other people's infrastructure since 1996, and inventory is by a distance the least glamorous and most decisive part of the job. You do not win it with a tool: you win it by knowing what runs inside each thing, who updates it, and what you do when the upstream project has not shipped anything yet. If your team has spent three days trying to answer "does this affect us?" and the answer is still an email thread, the problem is not this CVE: it is that the census does not exist. That is half of what we do in data and applications and in cybersecurity. If you want us to look at it with you, let us talk.

Sources. Everything below can be checked at source, and the links are there on purpose. The flaw: Node.js advisory of 21 October 2026 ("Node.js Brotli decoder out-of-bounds write permits remote code execution", severity High, fixed in 22.23.4, 24.22.1 and 26.12.1), the source of the literal description of the invalid block switch and the eight-byte internal buffer, the node:zlib reachability and the credits to OpenAI Codex Security (Khai Tran and Sylvie Mayer) and to mcollina and aduh95. The CVSS 4.0 score of 9.2 assigned by Google as CNA, the vector CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N and the CWE-787-over-CWE-252 classification, from the NVD entry. The upstream fix: commit 0ed4268 in google/brotli, "Detect and report invalid stream earlier", dated 27 February 2026, which adds the error code BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH and propagates error codes across the three block-switch functions; versions 1.0.10, 1.1.1 and 1.2.1 all published on 21 October 2026. Each product, in its own repository: dotnet/runtime #136422 (opened 24 October; builds 8.0.33, 9.0.22, 10.0.14 and 11.0.0-rc.3 given by Microsoft's security team in the thread on the 25th); apache/arrow #55166 (opened 23 October, still open as this article closed); ClickHouse #127419 (merged 25 October, the source of the own fork with the loongarch64 patch and the backports recorded to 26.10.3.6, 26.9.26.18, 26.8.35.17 and 26.3.61.13); Debian's security tracker, with its three source packages; and bulletin ISTIO-SECURITY-2026-007, which narrows the impact to those who enable Brotli decompression, for example with an EnvoyFilter. The fixed versions of Envoy (1.36.12, 1.37.8, 1.38.6, 1.39.3, 1.40.0) and Istio (1.29.9, 1.30.6, 1.31.2) predate the disclosure: they shipped between 6 and 16 October and the CVE was added to the bulletin afterwards. The analysis: JFrog Security Research ("Brotli2Shell", 25 October, by Aviv Engelberg, Eddy Tsalolikhin and Ofri Ouzan) and its open scanner. An important precision: in FFmpeg JFrog documents a segfault at 0x414141414141 freeing a pointer derived from the payload, and says it reproduced instruction-pointer control in "additional native applications" that it does not name; crediting FFmpeg with pointer control would go further than the source does. The two columns: the 91% and 82% are Wiz's own telemetry of 25 October across the environments it sees, not a census of the global estate, and Wiz does not bound the population; the sentence that "there is no public, reliable remote code execution proof-of-concept" and the 8.1 under CVSS 3.1 come from Red Hat's entry; the demonstration and its limits from Imperva, which ran code inside applications on "the official Node.js 24.22.0 Linux arm64 image" and warns that it took several attempts and that it did not test every build; the exploit is not published. CISA's SSVC entry of 23 October (exploitation "none", automatable "no"), the naming chronology — Python2Shell and HTTP2Shell on the 23rd, by Aikido — and the automatic behaviour of requests, urllib3, aiohttp and httpx when decoding Content-Encoding: br, from Phoenix Security's analysis. The fifth name, Broccoli2Shell, and its justification ("the bug is in Brotli itself and reaches well beyond Node.js"), attributed to researcher Juliano Rizzo's suggestion, from the independent page broccoli2shell.com, which declares itself unaffiliated with Google, the Node.js project and the Python Software Foundation. The patch states are as of 26 October 2026 and they change every few hours: before acting, go to the repository of the product that affects you, which is exactly what this article asks for. We did not find this flaw, we have not audited Brotli's code, and we do not have a client whose platform fell over because of it this week: this article reads public advisories and repositories and cross-reads them against how we keep an inventory ourselves, and says so deliberately.

Tags: Cybersecurity Vulnerabilities Inventory Patching Data Suppliers

Subscribe to our newsletter

To receive IT stories, everyWAN news and exclusive subscriber offers, sign up to our mailing list

Minorisa de Sistemas Informaticos y Gestión S.L. © 2026
everyWAN
everyWAN