Monday, November 2, 2026
Vulnerabilities

Brotli Decoder RCE Hits Node.js, Bun, Python and Envoy

CVE-2026-111497 is one C bug replicated across every runtime that ships Google's Brotli decoder. What's confirmed, what's claimed, and how to patch it all.

Illustration of one cracked component replicating a flaw across many connected machine parts.
One bug in Google's C decoder, copied into every runtime that vendors it.

A memory-corruption bug in Google's Brotli decoder — CVE-2026-111497 — turns out to be less like a normal library CVE and more like a distributed-side-quest boss fight. The flaw lives in the reference C decoder, which means every project that vendors that code carries its own copy: Node.js, Bun, the Python Brotli and brotlicffi packages, Envoy and Istio when Brotli decompression is enabled, and a long tail of bindings, proxies and CLI tools. The Hacker News reports that Imperva's Red Team has demonstrated actual code execution on an unpatched Node.js build — but only that one build, and nothing in the wild. Fixing this isn't one patch. It's a scavenger hunt through your transitive dependencies.

The flaw was disclosed on October 21, when Node.js and Google shipped fixes, with patched versions for other software following product by product. OpenAI's Codex Security team — specifically Khai Tran and Sylvie Mayer, credited by Node.js — reported it as unauthenticated remote code execution in Brotli, working in both directions: a malicious client attacking a server that decompresses uploads, or a malicious server attacking a client that fetches compressed responses. Google scores it 9.2 (critical) on CVSS 4.0; Node.js calls it high; Red Hat lands at 8.1. That spread in scoring is your first hint that the exploitability story is more nuanced than the headline.

How the bug actually works

The technical writeup at broccoli2shell.com walks the primitive cleanly, and it's a classic. A Brotli stream is split into blocks, and the decoder switches between block types as it reads. The function DecodeBlockTypeAndLength() in c/dec/decode.c returned a plain boolean — so callers couldn't distinguish a malformed stream from one that simply needed more input. On the fast path, the decoder ignored the return value entirely.

A crafted stream requests a block switch that can't happen. The decoder misreads that as a need for more input and keeps appending attacker-controlled bytes to an eight-byte internal buffer, writing past its end. That's the whole chain: an unchecked return value (CWE-252) becomes an out-of-bounds write (CWE-787), and the overwrite corrupts adjacent decoder state. From there, depending on heap layout and the host program, you get a crash, a memory leak, or — with enough grooming — code execution.

// Upstream fix: google/brotli commit 0ed4268 (plus 50a9b70, b418fa8, da98063)
// Before: boolean return — malformed stream indistinguishable from needing more input
// After:  hard format error returned to the caller
return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;  /* -17 */

Here's the part that should sting: the fix commit landed in the Brotli source tree on February 27. Nobody recognized it as a security fix at the time, and Brotli simply didn't cut a release for eight months. OpenAI's own account says the root cause "was actually fixed in the Brotli source code in February, but it wasn't known at the time that it gave rise to a security vulnerability and there was no more recent release of Brotli." The patch existed. It just sat in a repo while every downstream consumer kept shipping the vulnerable decoder. If you needed an argument for tracking vendored-dependency provenance, this is it.

Confirmed, claimed, and unknown — keep them separate

This is where discipline matters, because the signal-to-noise ratio on this one is rough. Three buckets:

  • Confirmed: The out-of-bounds write exists in Brotli decoder versions 1.0.3 through 1.2.0, fixed in 1.0.10, 1.1.1 and 1.2.1. Imperva's Red Team (project name Node2Shell) achieved code execution on an unmodified Node.js 24.22.0 build for Linux on ARM64, delivered both as a fetched Content-Encoding: br response and as an Express request body. The exploit took several tries.
  • Claimed but not demonstrated: OpenAI describes bidirectional, unauthenticated RCE affecting Node.js, Bun and Envoy, including the malicious-server-attacks-client scenario (think curl --compressed). Plausible given the primitive, but nobody has shown code execution on any software other than that one Node.js build. The exploit has not been released.
  • Unknown: Whether real-world exploitation will follow. CISA's October 23 assessment recorded exploitation as "none" and automation as "no." Red Hat notes there is no reliable public proof-of-concept for running code. Heap-grooming reliability across builds, allocators and architectures is an open question — ARM64 Linux proved tractable; that's all we know.

Why does the distinction matter? Because your patching priority should follow demonstrated exploitability times exposure, not the scariest sentence in the disclosure. The confirmed case is a server-side decoder reachable via HTTP. The bidirectional-RCE framing is the reporter's assessment of the primitive's ceiling, not a documented capability. Both argue for patching fast — a public bug with a known eight-byte heap overflow primitive is catnip for exploit developers — but nobody should be declaring incident over a PoC that doesn't exist yet.

You're exposed in more places than your dependency list admits

Exposure requires exactly one thing: your program decodes Brotli data someone else controls. The nasty part is how many paths do that without ever appearing in your code.

On Node.js, the built-in fetch() transparently decodes Content-Encoding: br responses, and Express 5 / body-parser 2 will inflate Brotli request bodies — so webhooks, link-preview fetchers, crawlers and "import from URL" features are all attack surface. Note the fine print from broccoli2shell.com: Express and body-parser don't contain the bug; they're just paths to it. Node bundles its own copy of Brotli, so your system libbrotli version is irrelevant there — only the runtime version matters. A bare node:http server doesn't decode bodies on its own.

Python is sneakier. If the Brotli or brotlicffi package is installed at all, then urllib3 (and therefore requests), aiohttp, httpx with the brotli extra, and crawlers like Scrapy will decode Brotli responses automatically. Your application can be fully exposed without the string "brotli" appearing anywhere in your source. That's a transitive-dependency ghost: the vulnerability isn't in what you wrote or even what you imported — it's in what your HTTP client quietly loaded.

# Don't trust the scanner — check by hand
pip show Brotli brotlicffi
# Vulnerable: Brotli <= 1.2.0, brotlicffi <= 1.2.0.x
# Fixed:      Brotli 1.2.1, brotlicffi 1.2.1.0

And you genuinely can't trust the scanner yet. The CVE record names Google's Brotli repository, not the PyPI packages, and at disclosure GitHub's advisory — which is what Dependabot reads — listed no affected package. PYSEC-2026-4322 and PYSEC-2026-4323 for the two Python packages only appeared on October 23. If this story has a moral for defenders, it's that CVE-driven tooling systematically under-counts vendored C code. We saw a lighter version of the same problem when a backdoored package slipped past ecosystem checks in the LiteLLM supply-chain incident: the advisory graph lags the actual artifact graph.

Illustration of hidden nested dependencies below the visible surface of a software stack.
The vulnerable decoder hides in transitive dependencies your manifest never lists.

Envoy, Istio, and the blast radius beyond runtimes

Envoy versions 1.31.0–1.31.1, 1.30.0–1.30.5 and 1.29.0–1.29.8 are affected only when Brotli decompression is configured — through a filter, for instance. Fixed releases are 1.31.2, 1.30.6 and 1.29.9. Istio follows the same conditional logic. If decompression is off, the bug is unreachable, which is at least a clean triage question for the platform team.

Beyond the named software, anything shipping or linking Brotli 1.0.3–1.2.0 may be affected even before an advisory says so: the system library (libbrotlidec, packaged as libbrotli1 and friends), Java JNI wrappers, PHP and Ruby extensions, Go cgo bindings, archive tools. The rewritten-in-other-languages decoders are fine — the Java, JavaScript and C# ports can't reach the bug, Rust's brotli-decompressor catches the condition and errors out, and the Go port (andybalholm/brotli) had the same logic error but could only crash, fixed October 23. Code execution is a C-decoder-only privilege. Language choice as exploit mitigation, once again earning its keep.

Mitigation: what works and what doesn't

First, the non-fixes. Dropping br from your client's Accept-Encoding header doesn't help — a malicious server can send Brotli regardless of what you asked for. WAF rules have a hard ceiling too: Vercel, which shipped firewall rules on disclosure day, said they don't cover "responses your functions fetch from other servers." For the outbound-fetch path, only updating the runtime or library closes the hole. And for Node.js specifically, no workaround has been published at all — upgrading the runtime is the only fix.

What does work, per OpenAI's disclosure page and security firm Aikido, where you can't patch immediately:

  • Reject incoming requests with Content-Encoding: br at the edge, before any proxy or parser touches the body.
  • Disable automatic Brotli decoding in HTTP clients, and reject Brotli responses before they're decoded.
  • In Python, uninstall Brotli and brotlicffi entirely so clients fall back to gzip and deflate.
  • After patching, restart the service and rebuild container images — a running process keeps the old decoder in memory until it restarts, and an image keeps it until rebuilt on a patched base.

The version matrix, condensed: Node.js 22.23.4, 24.22.1 and 26.12.1 are fixed (20.x and older are end-of-life — migrate); Bun 1.4.4 is fixed; Python Brotli 1.2.1 and brotlicffi 1.2.1.0 are fixed. On the distro side, Debian patched trixie's brotli package on October 23 (1.1.0-2+deb13u1), while Amazon Linux 2023 still listed brotli and Node.js 24 as pending as of October 22. Expect that lag; it's exactly why "we patched" needs to mean the running process, not the package manager's todo list.

Zoom out and this is the same lesson the ecosystem keeps relearning at boss-fight difficulty: vendored C code is a liability multiplier. One unchecked boolean in a decoder, copied into a dozen runtimes, and the remediation cost is paid per-consumer, per-restart, per-rebuild. Credit where due — Khai Tran and Sylvie Mayer found and reported it, Node.js maintainers Matteo Collina and Antoine du Hamel shipped the fix fast, and the broccoli2shell.com writeup is the clearest technical reference available. No in-the-wild exploitation yet. Patch before someone publishes the PoC that changes that sentence.

Sources

This article draws on reporting and advisories from: