November 2, 20263 min read
HTTP2Shell: Why One Brotli Bug Reached Node.js, Python, .NET and Envoy
CVE-2026-111497 isn't an HTTP/2 flaw. It's an out-of-bounds write in Google's Brotli decoder, and it can be reached both inbound and outbound. How to find every copy you ship.
The name is misleading. HTTP2Shell (also called Node2Shell, tracked as CVE-2026-111497) has nothing to do with the HTTP/2 protocol. It's an out-of-bounds write in Google's Brotli decompression library, disclosed alongside the Node.js security releases on 21 October 2026 and scored 9.2 (Critical) under CVSS 4.0.
It matters because Brotli is everywhere: it's what
Content-Encoding: br means. When a vulnerable decoder processes a crafted
stream, the result can be a crash, leaked process memory or, under the right
conditions, remote code execution.
Where the Bug Lives
In vulnerable versions, a fast-path macro in Brotli's block-decoding logic doesn't properly check the return value when a stream switches block type. The decoder keeps going in an invalid state, and a crafted stream turns that into an out-of-bounds write. The fix makes the decoder return a distinct error and stop.
Fixed upstream in Brotli 1.0.10, 1.1.1 and 1.2.1. Downstream fixes include:
- Node.js: 22.23.4, 24.22.1, 26.12.1
- Bun: 1.4.4
- .NET: 8.0.33, 9.0.22, 10.0.14
- Python:
Brotli1.2.1,brotlicffi1.2.1.0 - Envoy: 1.40.0, 1.39.3, 1.38.6, 1.37.8, 1.36.12
- Java brotli4j: 1.24.0
- Firefox and Thunderbird, plus distribution
libbrotlipackages
Two Ways In
Most write-ups focus on servers, but exposure runs in both directions.
Inbound. A client sends a request body with Content-Encoding: br to a
server or middleware that decompresses it automatically. Most apps never
expect compressed request bodies, which also makes this easy to spot in logs.
Outbound. Your service fetches a URL and the response is Brotli-encoded:
fetch() calls, webhook deliveries, link previews, crawlers, scrapers, API
integrations. A malicious or compromised server can send a crafted response
to a client that decodes it automatically. Any feature that fetches
user-supplied URLs is a delivery mechanism.
Zscaler reported reconnaissance and exploitation attempts against its decoys within 24 hours of disclosure.
The Hard Part: Finding Every Copy
Patching the OS package isn't enough, because Brotli gets vendored:
- Runtimes bundle it. Node.js and Bun ship their own copy, so updating
libbrotlion the host doesn't touch them. - Container images freeze it. Every image built before the fix carries the old decoder until it's rebuilt, whatever the host says.
- Proxies embed it. Envoy and service meshes like Istio decompress traffic before your app sees it.
- Native wheels and JARs statically link it. Check
brotli,brotlicffiandbrotli4jin lockfiles.
A practical order of work:
- Query SBOMs and lockfiles for
brotliin any form. - Upgrade runtimes to the fixed versions above and rebuild every image.
- Prioritize internet-facing services that accept compressed bodies, then anything that fetches external or user-controlled URLs.
- Where patching is delayed, turn off Brotli request decompression at the edge if your stack allows it, and restrict egress for URL-fetching services.
End-of-Life Runtimes Are the Real Risk
Fixes shipped for the supported Node.js lines (22, 24 and 26). Node.js 20 reached end of life in April 2026, so it gets no official patch, whatever its exposure. If anything you run, or any build or function runtime your hosting provider uses for you, is still pinned to an EOL line, moving to a supported LTS is now a security task.
What to Watch For
- Requests with
Content-Encoding: brto endpoints that never receive compressed bodies - Crashes or segfaults in Node.js, Bun, Python, .NET or Envoy workers after handling compressed input
- Unexpected child processes spawned by web runtimes
- New outbound connections right after a worker crash