Node2Shell: Google's Brotli RCE fix sat in public for 236 days

On this page · 12 sections
- Key numbers
- The fix was public in February. The release came in October
- What the bug actually does, in plain English
- The downstream lag: everyone waited for Google
- 81 releases shipped the hole after the fix existed
- Silent fixes are a gift to attackers
- The AI found it. The humans sat on it
- Day 3 on PyPI: 1 in 3 installs is still the bad one
- The Rust port copied the bug line for line. Rust contained it
- Who wins and who loses
- What this means for you
- Sources
Short answer: The Brotli vulnerability everyone is calling Node2Shell (CVE-2026-111497, CVSS 9.2) was fixed in Google's public Brotli repo on February 27, 2026, under the commit message "Detect and report invalid stream earlier." No release carried that fix until October 21. For 236 days the patch was one git diff away for any attacker, while Node.js, Envoy, Bun and brotlicffi shipped 81 more releases on the broken decoder and PyPI served 658 million vulnerable Brotli downloads.
TL;DR: We pulled the commit dates, every downstream release and PyPI's per-version download logs. The bug has been in Brotli since v1.0.3 (March 2, 2018), so it lived 3,155 days, and an AI security tool from OpenAI found it, not a human audit. Two days after the PyPI fix, 37% of fresh Brotli installs are still the vulnerable version, and the Rust port carries the exact same logic hole today, with no patch and no issue filed, but memory safety turns its worst case from remote code execution into a crash.
Key numbers
| What | Number | Source, checked Oct 24 2026 |
|---|---|---|
| Days from Google's public fix commit to a release | 236 (Feb 27 to Oct 21) | google/brotli commit 0ed4268, v1.2.1 |
| How long the bug existed | 3,155 days (since v1.0.3, Mar 2 2018) | Brotli releases, GHSA-9v8h-qqgm-4m35 |
| Severity | CVSS v4 9.2, Critical (Node.js rates it High) | NVD, Node.js advisory |
| Downstream releases shipped after the fix existed, still vulnerable | 81 (Node.js 41, Envoy 30, Bun 8, brotlicffi 2) | Our count from nodejs.org/dist and GitHub/PyPI release dates |
| Vulnerable Brotli downloads from PyPI while the fix sat in public | 657,591,827 | Our query of PyPI logs (ClickHouse public pypi dataset) |
| Share of Brotli PyPI downloads on the patched 1.2.1, Oct 22 / 23 / 24 | 17.7% / 42.6% / 63.2% | Same query, pypi_daily.csv |
| Vulnerable Brotli downloads since the CVE went public (Oct 22 to 24) | 7,119,516 | Same query |
| Python HTTP clients that decode br automatically when Brotli is installed | urllib3 (1.32B downloads a month), requests (1.20B), httpx, aiohttp, Scrapy | Aikido, pypistats |

The fix was public in February. The release came in October
Here is the timeline from GitHub's own API. On February 27, 2026, Google engineer Evgenii Kliuchnikov landed commit 0ed4268 on Brotli's main branch. The message: "Detect and report invalid stream earlier." Nothing in it says security, nothing says CVE.
That commit is the fix for CVE-2026-111497. The same commit was cherry-picked into the 1.0, 1.1 and 1.2 branches on October 21 and shipped as v1.0.10, v1.1.1 and v1.2.1 that afternoon. The last Brotli release before that was v1.2.0, in October 2025. In between: 236 days with the patch sitting on a public branch and zero releases.
The Aikido write-up puts it plainly: the root cause was fixed in February, "but it wasn't recognized as a security issue at the time." OpenAI reported it as unauthenticated remote code execution in February. So either Google read a report from OpenAI's security team and filed it as a stream-validation tweak, or the report and the commit never met. Neither version is a good look for the company that runs the CVE process for its own library (Google is the CNA here).
What the bug actually does, in plain English
Brotli is the compression format behind a big slice of the web. When a server or client receives Brotli data, a decoder unpacks it. That decoder is a C library from Google, and almost everything bundles it: Node.js, Python's Brotli package, Bun, Envoy, curl builds.
Brotli streams can switch between "block types" mid-stream. The function that handles a switch, DecodeBlockTypeAndLength(), returned a plain true or false. False meant two different things: "this stream is broken" or "I need more bytes." On the fast path, callers threw the answer away. A crafted stream declares one block type, burns through its length and forces a switch that cannot succeed. The decoder then asks for more input forever, or keeps decoding with stale state. Node's advisory says the streaming decoder "kept appending input bytes to an eight-byte internal buffer." That is an out-of-bounds write: memory corruption, memory leaks, crashes, and possibly code execution.
Here is the heart of the fix, from our diff of c/dec/decode.c between v1.2.0 and v1.2.1:
- static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
+ static BROTLI_INLINE BrotliDecoderErrorCode DecodeBlockTypeAndLength(
...
if (max_block_type <= 1) {
- return BROTLI_FALSE;
+ return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
}
One boolean turned into an error code. That is the whole thing. A two-line idea that took eight and a half years to find and eight months to ship.
It cuts both ways. A malicious client can hit a server that decodes request bodies (Express 5 and body-parser 2.x do). A malicious server can hit any client that accepts br responses, which is Node's built-in fetch() and, in Python, urllib3, requests, httpx, aiohttp and Scrapy the moment Brotli is installed. Removing br from your Accept-Encoding header is not enough: a hostile server can send Brotli anyway.

The downstream lag: everyone waited for Google
We pulled the release date of every fixed version we could find. Counting from the February 27 commit: Envoy shipped fixed builds (1.39.3, 1.38.6, 1.37.8) on October 6, day 221, two weeks before the public disclosure. Istio followed on October 15. Node.js shipped 22.23.4, 24.22.1 and 26.12.1 on day 236. Bun 1.4.4, Brotli 1.2.1 on PyPI and brotlicffi 1.2.1.0 landed on day 237. The Go port, andybalholm/brotli, copied Google's fix on October 23, day 238, under the same commit title.
Envoy's early date matters. Some projects clearly got the memo before the rest of the world. That is how coordinated disclosure is supposed to work, but it also means the fix existed, was understood as a security issue by at least one downstream by early October, and the upstream release still waited.
81 releases shipped the hole after the fix existed
This is the part nobody else has counted. Between February 27 and each project's patch, these projects kept shipping new versions with the vulnerable decoder inside:
- Node.js: 41 releases across the 20, 22, 24, 25 and 26 lines (from nodejs.org/dist/index.json). Node 20 went end-of-life in April, so its last two builds, 20.20.1 and 20.20.2, will never get the fix.
- Envoy: 30 releases across five branches before October 6.
- Bun: 8 releases, 1.3.11 through 1.4.3.
- brotlicffi: 2 releases, 1.2.0.1 (March 5) and 1.2.0.2 (August 21), both built on the vulnerable 1.2.0 decoder, per OpenSourceMalware.
None of that is those maintainers' fault. They bundle what upstream tags. That is exactly the point: when the upstream sits on a fix without a release or an advisory, every downstream keeps shipping the bug with a fresh version number on it, and every user who "stays up to date" stays vulnerable.

Silent fixes are a gift to attackers
Attackers read commit logs. Diffing a security-relevant library's main branch for changes to error handling is a standard way to find bugs before they get a CVE. A commit that changes a decoder's return contract on malformed input, in a C library parsing untrusted network data, is precisely the kind of diff that gets looked at.
We cannot prove anyone found it that way. There was no public report of exploitation in the wild as of October 22, per The Hacker News coverage cited in the Aikido write-up. But the window was open for 236 days, and the only thing standing between an attacker and the bug was that the commit message said "invalid stream" instead of "RCE." That is security by vague commit message.
Our bet: this is not a one-off. When AI tools hand maintainers a pile of reports at once, some get triaged as "robustness" and quietly fixed. The fix lands, the CVE does not, and downstreams never hear the bell. Expect more "fixed in February, disclosed in October" timelines in 2027, not fewer.
The AI found it. The humans sat on it
Look at the credit line in Node's advisory: "Thank you, to OpenAI Codex Security - Khai Tran and Sylvie Mayer for reporting this vulnerability." The bug lived in one of the most deployed decoders on earth for 3,155 days. Years of human review and fuzzing never surfaced it as a security bug. An AI security product did.
So the anti-AI talking point that AI makes software less secure needs a correction. In this case the machine found the bug, and the slow part was the human pipeline: triage, labeling, release, advisory. OpenAI is not a saint here either; it is marketing Codex Security off this find (OpenAI's post). Fine. The receipt still says the AI was eight months early and the release process was eight months late.
If you write about AI and code, this is the fight worth having. We wrote about how to review AI-generated code; the flip side is letting AI review the human-written C you already ship.
Day 3 on PyPI: 1 in 3 installs is still the bad one
We queried PyPI's download logs per version (ClickHouse hosts a public copy of PyPI's BigQuery data). Brotli is not a niche package: 82.8 million downloads a month.
- October 21: 0% of Brotli downloads were a patched version (none existed).
- October 22, the day 1.2.1 shipped: 17.7%.
- October 23: 42.6%.
- October 24 (a Saturday): 63.2%. The other 36.8%, about 956,000 downloads, were still vulnerable versions, mostly 1.2.0, 1.1.0 and 1.0.9.
Since the CVE went public, PyPI has served 7.1 million vulnerable Brotli downloads. Those are mostly lockfiles and pinned CI images doing exactly what they were told. And the Aikido post flags a nasty detail: as of October 23, PyPI advisory databases had not mapped the CVE to Brotli yet, so pip-audit, Dependabot and Safety would not have warned you. Advisories PYSEC-2026-4322 and PYSEC-2026-4323 were added on October 24.
brotlicffi is slower: 55.1% patched on October 24.

The Rust port copied the bug line for line. Rust contained it
Here is where it gets fun for the Rust crowd. The Rust ecosystem's Brotli is Dropbox's brotli-decompressor (305 million downloads on crates.io, pulled in by the brotli crate with 614 dependents, and by async-compression, which sits under a lot of reqwest and tower-http setups). It is a faithful port of Google's C decoder. Faithful enough that it ported the bug.
From the current src/decode.rs on master, checked October 24:
fn DecodeBlockTypeAndLength<...>(safe: bool, ...) -> bool {
...
if max_block_type <= 1 {
return false;
}
And on the fast path, the caller ignores the result the same way the C did:
if (!DecodeLiteralBlockSwitchInternal(safe, s, input)) && safe {
result = BrotliDecoderErrorCode::BROTLI_DECODER_NEEDS_MORE_INPUT;
Its error enum stops at FORMAT_DISTANCE = -16; there is no -17 FORMAT_BLOCK_SWITCH. The last release, 6.1.1 on October 21, is a version bump, and no issue or commit mentions the CVE. The Go port patched in two days. The Rust port has not patched at all.
So why is nobody panicking about Rust? Because the default build is safe Rust. The crate's own manifest says that without --features=unsafe, "all included code is safe," with array bounds checks on. The same stale-state logic that writes past an eight-byte buffer in C hits a bounds check in Rust. Our reading of the code, not a test we ran: the worst case there is a panic, which is a denial of service on that request, not an attacker running code on your box. If you enabled the unsafe feature for speed, you turned the bounds checks off, and all bets are off.
That is the whole Rust argument in one CVE. Same humans, same algorithm, same logic bug, copied faithfully. One language turned it into remote code execution in Node, Python and Bun; the other turned it into a crash. We track the ecosystem side of this on our Rust hub, and our earlier piece on GitHub outages and where Rust crates live shows how concentrated this supply chain already is.

Who wins and who loses
- Wins: OpenAI. It gets a 9.2 CVE in Node, Python, Bun and Envoy as a sales demo for Codex Security. Lowkey the best ad it has run all year.
- Wins: memory-safe ports. Go patched in two days; Rust's default build cannot be turned into an RCE by this bug even unpatched.
- Loses: Google's credibility as a steward. It owns the library, it is the CNA, and its own commit sat public for 236 days with a message that hid the stakes.
- Loses: everyone who trusted "latest." 81 downstream releases, 658 million PyPI downloads and every Node 20 user who never gets a patch.
- Loses: the scanners. Dependabot and pip-audit were blind to the Python side for at least a day after disclosure.
What this means for you
Do these today, in this order:
- Node.js: upgrade to 22.23.4, 24.22.1 or 26.12.1 or newer. On Node 20, move off it; no fix is coming. Rebuild and redeploy container images, because the runtime is baked in.
- Python:
pip install -U "brotli>=1.2.1" "brotlicffi>=1.2.1.0", regenerate lockfiles and rebuild wheels and images. If you do not need Brotli, uninstalling it makes urllib3, requests and aiohttp fall back to gzip. - Bun, Envoy, Istio: Bun 1.4.4, Envoy 1.40.0 / 1.39.3 / 1.38.6 / 1.37.8, Istio 1.29.9 / 1.30.6 / 1.31.2 or later. Recompile anything you shipped as a Bun single-file executable.
- Rust: make sure nothing in your tree enables
brotli-decompressor/unsafeorbrotli/unsafe(cargo tree -e features -i brotli-decompressor), and watch the repo for the port of the fix. - At the edge: if you cannot patch tonight, reject inbound requests with
Content-Encoding: brat the proxy, and rejectbrresponses before decoding on outbound clients.
Stuck in a dependency upgrade that will not build, a lockfile that refuses to move or a Rust build that breaks on the new version? That is exactly what our paid fix covers: we do the security-advisory upgrade for you, and we keep fixing it at no extra cost until it builds and passes. How paid fixes work.
More on the AI and security beat on our AI industry hub and the news desk. Download the data below and check our numbers: data.csv has every project and date, pypi_daily.csv has every day of PyPI downloads since February. Our question for you: should a fix for a 9.2 RCE be allowed to sit on a public branch for eight months without a release, or is "silent fix first, CVE later" the only sane way to ship security patches?
Sources
- google/brotli commit 0ed4268, "Detect and report invalid stream earlier", Feb 27 2026, and releases v1.0.10, v1.1.1, v1.2.1, Oct 21 2026
- Node.js, October 2026 security releases, Oct 21 2026
- GitHub Advisory GHSA-9v8h-qqgm-4m35, Oct 22-23 2026; NVD CVE-2026-111497
- Aikido, "Node2Shell evolves to HTTP2Shell", Oct 23-24 2026
- OpenSourceMalware, Node2Shell affects Python, Oct 23-24 2026
- OpenAI on X, Oct 23 2026
- nodejs.org release index; GitHub releases for Envoy, Bun, Istio; andybalholm/brotli 33c2a98; brotlicffi release history, all pulled Oct 24 2026
- dropbox/rust-brotli-decompressor source and crates.io stats, Oct 24 2026
- PyPI per-version downloads via the ClickHouse public pypi dataset and pypistats, our queries Oct 24 2026
- Hacker News discussion (281 points), Oct 24 2026
First published here on tensorust.com on Oct 24, 2026. Related: Paid fixes, Rust hub, AI industry.
Up next
More on AI industryWikipedia lost 12% of its human readers in 2 years. Bots: 1 in 3 viewsEnglish Wikipedia human pageviews fell 12% in two years to a record low while bots hit 34% of views. Wikimedia's revenue per reader doubled. Free CSV.Read it next →Cite this
Free to cite and reuse under CC BY 4.0 with a link back. How it was collected: methods. Spotted an error? Corrections.
Did this answer your question?
Get the next one by email.
What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.
No spam, never sold. Every email has a one-click unsubscribe.
What are people saying about this? Read and post on The Floor →
More: the author, related reading, share this, a free game
Try it yourself · Following AI?
Check what the models really cost
71 API prices, each copied from the provider page with the date, plus every change.
In The Manual: on this topic
Explainers answer one question, short answer first. Guides walk you through it step by step.
- Explainer · Shipping and sellingSnowflake ID vs UUID: which should you use?
- Explainer · Shipping and sellingUUID vs NanoID: which ID should you use?
- Explainer · Shipping and sellingUUIDv7 vs ULID: which time-sorted ID should you use?
- Explainer · Money and the economyWhat is circular financing in AI?
Read next
- AI · Oct 24, 2026 · 9 min readWikipedia lost 12% of its human readers in 2 years. Bots: 1 in 3 views
- AI · Oct 24, 2026 · 9 min readNvidia throttled RTX 3090s for 6 years. One register gives back 66%
- AI · Oct 24, 2026 · 14 min readAI isn't overhyped. Prices fell 99% and it feels like nothing changed