Skip to content

HTTP2Shell: the Brotli Decoder Bug That Is Already in Your Runtime and Nobody Imported It on Purpose

HTTP2Shell CVE-2026-111497: Brotli Decoder RCE in Node, Bun, Python

HTTP2Shell: the brotli decoder bug that is already in your runtime and nobody imported it on purpose

here is the part that stings. you did not install brotli. you did not write a single line that calls it. and yet node, bun, python, envoy, istio, .net and about ninety percent of cloud environments are carrying a copy of a decoder with an out-of-bounds write, because compression rides along for free with “decode this response for me.” CVE-2026-111497 went from a quiet upstream commit to a cross-ecosystem fire drill in three days, and the nickname HTTP2Shell has nothing to do with HTTP/2. it is just that Content-Encoding: br is the shortest road to the broken function.

What this is. CVE-2026-111497 (also called Node2Shell, Python2Shell, and Broccoli2Shell, one bug) is an out-of-bounds write in google’s reference C brotli decoder, c/dec/decode.c. versions 1.0.3 through 1.2.0 are affected. a crafted brotli stream corrupts adjacent decoder state, which can leak memory, crash the process, or under the right heap conditions reach remote code execution. CNA CVSS 4.0 is 9.2 critical. GHSA-9v8h-qqgm-4m35. reported by Khai Tran and Sylvie Mayer of OpenAI Codex Security, with the node fix by Matteo Collina and Antoine du Hamel. not on CISA KEV as of this writing, and no confirmed in-the-wild exploitation yet.

the one-paragraph version

the bug lives in block-switch handling. an internal function returned a plain boolean, so the caller could not tell “this input is malformed” apart from “i need more bytes.” the fast path dropped the return value entirely, which is why the CVE record carries CWE-252 (unchecked return value) alongside CWE-787 (out-of-bounds write). a crafted stream drives the decoder into a block switch that valid input never produces. the decoder reads the failure as “send more input” and keeps appending attacker bytes into a buffer sized for eight of them. bytes past that boundary land on neighboring decoder state. upstream commit 0ed4268 makes the function return a real error code, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (error -17), on both paths. the funny part: this fix sat in public source since february, unlabeled, for eight months before anyone called it security.

why this one is worse than a normal library CVE

two reasons. first, it runs in both directions. inbound, a hostile client sends a request body with Content-Encoding: br to a server that inflates it (express 5 and body-parser 2.x do this by default; a bare node http server does not). outbound, a hostile server returns a br response to any client that auto-decodes it: node and bun fetch(), python requests, urllib3, aiohttp, httpx, libcurl. webhooks, link previews, URL importers, image proxies, OIDC discovery, and browsing AI agents are all outbound victims, and your inbound WAF rules never see a single byte of it.

second, most vulnerable copies are vendored. the decoder is baked into node, bun, envoy, the .net runtime, cdxgen binaries, and python wheels. it never shows up in a lockfile, so manifest-based SCA happily reports you clean. distro backports cut the other way: debian’s fixed 1.0.9-2+deb12u1 still reads as 1.0.9, so naive version matching throws false positives. node cherry-picked the fix, so judge node by the node version, not by process.versions.brotli.

Kali terminal inventorying node, bun, and python brotli versions and flagging each as vulnerable to CVE-2026-111497.
Step 0: the whole fight is inventory. node 20.x (EOL, brotli 1.1.0), bun 1.4.2, and PyPI brotli 1.2.0 are all below their fixes. you did not import any of them on purpose.

a safe lab repro of the decoder path

we are not shipping a weaponized invalid-block-switch stream. that is the exact primitive every serious writeup is deliberately leaving out, and we are not the ones to break that. what we can show, on a box we own with a genuinely vulnerable brotli 1.2.0, is that the decoder is reachable and what a malformed stream does to it. round-trip works, then we corrupt the compressed bytes and watch the decoder fail. on a patched build a malformed block switch returns a clean format error and stops. on a vulnerable build, a carefully crafted block switch (not this one) keeps appending attacker bytes past the eight-byte buffer. same logic error, two very different endings.

Kali terminal running a safe Python brotli lab script that round-trips data and then raises brotli.error on a corrupted stream, demonstrating the decoder path without weaponization.
Step 1: the decoder is in the path (round-trip ok), and a mangled stream fails. the difference between “fails cleanly” and “overruns the buffer” is the whole CVE, and the weaponized version stays off this page.

shutting the inbound door while you patch

the fastest inbound mitigation is to reject br request bodies before any parser touches them, on direct-to-origin routes as well as behind the CDN. in express, put a guard ahead of every body parser, or set inflate: false. memory-safe ports, for what it is worth, hit the same logic bug but fail safely: the go port andybalholm/brotli can only panic, and the rust brotli-decompressor returns an error. pure java, javascript and c# ports (pdf.js, iText, HtmlUnit) are not affected at all.

Kali terminal showing an Express middleware that rejects Content-Encoding br request bodies with 415 and a curl test confirming the body never reaches the parser.
Step 2: a 415 before the parser keeps inbound br off the decoder. the outbound path is nastier, because dropping br from Accept-Encoding does nothing, a server can send brotli regardless.

hunting for probes and triggers

none of these are secret IOCs, they are the public advisory signals. inbound, legitimate clients almost never send brotli request bodies to a JSON API, so br content-encoding on those routes is suspicious on its own. patched node and brotli log a clean BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (error -17) you can alert on, which means the fix caught a probe. on unpatched hosts the tell is uglier: a SIGSEGV or SIGABRT in node, bun, python, or envoy right after a br request or an outbound fetch to a user-supplied host.

Kali terminal grepping access logs for brotli content-encoding, patched decoder error -17, and crash signatures tied to CVE-2026-111497.
Step 3: br content-encoding on JSON routes, error -17 on patched builds, and crashes after untrusted br input. a crash after a br body or fetch means assume trigger, because memory disclosure leaves no clean trail.

the patch matrix, because the numbers are a mess

  • Google brotli / libbrotli: 1.0.3 to 1.2.0 vulnerable, fixed in 1.0.10, 1.1.1, 1.2.1.
  • Node.js: unpatched 22.x / 24.x / 26.x, fixed in 22.23.4 / 24.22.1 / 26.12.1. node 18, 20, 23, 25 are end of life with no fix, so move off them. the OS patch does not fix the official node binaries or docker images, they bundle their own copy.
  • Bun: 1.0.21 to 1.4.3 vulnerable, fixed in 1.4.4. rebuild any bun build --compile executables.
  • Python: pip install -U "Brotli>=1.2.1" "brotlicffi>=1.2.1.0", then restart long-running workers. watch the trap: older urllib3 guidance told people to move to 1.2.0, which is the last vulnerable release. extras like urllib3[brotli], httpx[brotli] and aiohttp[speedups] pull it in.
  • Envoy / Istio: fixed, but only matters where the brotli decompressor is actually configured. envoy 1.36.12 / 1.37.8 / 1.38.6 / 1.39.3 / 1.40.0; istio 1.29.9 / 1.30.6 / 1.31.2.
  • Still unpatched as of now: the .net runtime (BrotliStream, HttpClient auto-decompression), DuckDB, ClickHouse, Apache Arrow bundled builds, 7-Zip forks, EDK II, and woff2 Makefile builds. quarantine third-party parquet with brotli columns until the data tools ship fixes.

the order of operations

  1. inventory runtimes including base images: docker run --rm node -v, bun --version, and the python brotli check. judge node by node version, not process.versions.brotli.
  2. upgrade node, bun, brotli, brotlicffi. rebuild and redeploy every image and compiled executable, because a running process keeps the old decoder until it restarts.
  3. reject inbound br bodies at the edge and on direct-to-origin routes; turn off automatic response decoding where services fetch attacker-chosen URLs.
  4. regenerate SBOMs from built images, not source manifests, and diff against the patch matrix. re-run pip-audit and dependabot, the GHSA was reviewed mid-week and the mappings changed daily.
  5. rotate credentials held by any internet-facing process that ran a vulnerable decoder on untrusted br input. memory disclosure does not announce itself.

sloth take

the scariest vulnerabilities are not the exotic ones, they are the boring plumbing you forgot you were running. compression is plumbing. nobody writes “decompress untrusted brotli” in a design doc, it just happens because a client library decided to be helpful and a runtime decided to bundle a decoder. HTTP2Shell is going to live in people’s stacks for months, not because the patch is hard, but because the copies are invisible. go check your node version. then go check the thing that fetches URLs on your behalf, because that is the one nobody is looking at.

sources

  • CVE-2026-111497, GHSA-9v8h-qqgm-4m35, PYSEC-2026-4322 (Brotli), PYSEC-2026-4323 (brotlicffi)
  • Wiz research writeup on Broccoli2Shell (October 25, 2026), affected/fixed version table and cloud exposure data
  • Phoenix Security HTTP2Shell / Node2Shell deep dive: root cause in c/dec/decode.c, commit 0ed4268, error -17, inbound and outbound paths, detection signals
  • Node.js October 2026 security releases (22.23.4 / 24.22.1 / 26.12.1); Bun GHSA-663w-cv8g-jj2p; Envoy GHSA-3wvj-j6qm-62mr; ISTIO-SECURITY-2026-007
  • Imperva red team demonstration of code execution on Node.js 24.22.0; Aikido and OpenSourceMalware analysis of the Python ecosystem impact
  • google/brotli releases v1.0.10, v1.1.1, v1.2.1