Contents
ToggleExecutive Summary
HTTP2Shell, first circulated as Node2Shell, is an out-of-bounds write in Google’s reference C Brotli decoder (c/dec/decode.c), tracked as CVE-2026-111497 and GHSA-9v8h-qqgm-4m35. Brotli 1.0.3 through 1.2.0 is affected. A crafted Brotli stream writes past an internal buffer and corrupts adjacent decoder state, which can leak memory, crash the process, or, under the right heap conditions, hand the attacker code execution. The nickname is unrelated to the HTTP/2 protocol; it reflects that HTTP content decoding is the most common path to the decoder.
The bug became a cross-ecosystem problem within three days. Node.js and Bun bundle the library. So do Envoy, the .NET runtime, Chromium, cdxgen’s standalone binaries and a long list of data and document tools. On the Python side, the PyPI Brotli and brotlicffi packages ship the vulnerable decoder, and requests, urllib3, aiohttp and httpx will use either one automatically to decode Content-Encoding: br responses. An application can be exposed without a single import of Brotli in its own code.
Patches exist for the main runtimes; the harder work is finding where the decoder is reachable. The attack runs in both directions: a hostile client can hit a server that inflates request bodies, and a hostile server can hit any client that decodes compressed responses. Webhooks, link previews, crawlers, URL importers and browsing AI agents are in scope, and inbound WAF rules do not cover any of them. Most vulnerable copies are vendored inside runtimes, wheels and static binaries, so manifest-based SCA reports them as clean.

TL;DR for Engineering Teams
| What it is | Out-of-bounds write in the Brotli C decoder, CVE-2026-111497, CVSS 4.0 9.2 Critical from the CNA (CWE-787 caused by CWE-252, an unchecked return value) |
| Where it bites | Brotli 1.0.3 to 1.2.0 wherever it is linked or vendored: Node.js 22/24/26, Bun 1.4.3 and earlier, PyPI Brotli and brotlicffi, Envoy and Istio with the Brotli decompressor, .NET BrotliStream and HttpClient auto-decompression, cdxgen standalone binaries and images |
| Why it matters | Unauthenticated and reachable both inbound (request bodies) and outbound (fetched responses). Imperva showed code execution on a stock Node.js 24.22.0 arm64 build. CISA SSVC on 23 October: exploitation none, automatable no, technical impact total. Not in KEV |
| Patch status | Partial. Upstream 1.0.10 / 1.1.1 / 1.2.1; Node.js 22.23.4 / 24.22.1 / 26.12.1; Bun 1.4.4; PyPI Brotli 1.2.1 and brotlicffi 1.2.1.0; Envoy and Istio fixed. .NET, PHP, Ruby, DuckDB, ClickHouse, Arrow, libjxl, MuPDF, 7-Zip forks and EDK II unpatched |
| Immediate action | Upgrade Node.js and Bun, rebuild images, pip install -U “Brotli>=1.2.1” “brotlicffi>=1.2.1.0”, restart processes. Until then, reject br request bodies at the edge and turn off automatic response decoding where services fetch attacker-chosen URLs |
Vulnerability Overview
| Field | Value |
|---|---|
| Names | HTTP2Shell, Node2Shell, Python2Shell, Broccoli2Shell (one bug) |
| Vendor | Google (CNA and upstream); redistributed by Node.js, Bun, PyPI maintainers, Envoy, Microsoft and others |
| Product | Brotli reference C decoder, c/dec/decode.c |
| Vulnerability type | Out-of-bounds write, heap memory corruption (CAPEC-100) |
| CWE | CWE-787 Out-of-bounds Write; CWE-252 Unchecked Return Value |
| CVSS | 4.0 9.2 Critical (CNA): 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. Node.js: High. Red Hat and Amazon Linux: 8.1 (CVSS 3.1). Istio: 7.7 for its own exposure. NVD: awaiting analysis |
| Identifiers | CVE-2026-111497, GHSA-9v8h-qqgm-4m35, PYSEC-2026-4322 (Brotli), PYSEC-2026-4323 (brotlicffi), Envoy GHSA-3wvj-j6qm-62mr |
| Reported by | Khai Tran and Sylvie Mayer, OpenAI Codex Security. Node.js fix by Matteo Collina and Antoine du Hamel |
| Patch available | Yes upstream and in major runtimes; no for many vendored copies |
| Active exploitation | Not confirmed. CISA SSVC: exploitation none (23 October). Red Hat: no reliable public PoC for code execution |
| CISA KEV | Not listed as of 24 October 2026 |
Technical Anatomy
Root cause
The flaw sits in block-switch handling. DecodeBlockTypeAndLength() returned a boolean, so callers could not tell “this input is malformed” from “I need more bytes.” The decoder’s fast path also dropped that return value, which is why the CVE record carries CWE-252 alongside CWE-787.
A crafted stream can drive the decoder into a block switch that valid input never produces. The decoder reads the failure as a request for more input and keeps appending attacker bytes to an internal buffer sized for eight bytes. Bytes past that boundary land on adjacent decoder state.
Upstream commit 0ed4268 (“Detect and report invalid stream earlier”) makes the function return a real error code on both the safe and fast paths: BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17). Node.js cherry-picked that commit and exposes the new constant. Memory-safe ports hit the same logic error but fail safely: the Go port andybalholm/brotli can only panic, and the Rust brotli-decompressor returns an error.
Exploit path
- Entry point. The attacker puts a Brotli stream in front of a decoder. Inbound, that is a request body with Content-Encoding: br sent to a parser that inflates it (Express 5 and body-parser 2.x do this by default; a plain Node.js http server does not decode bodies on its own). Outbound, it is a response from an attacker-controlled host read by Node or Bun fetch(), urllib3, requests, aiohttp, httpx, Scrapy or libcurl with automatic decoding. File inputs also work: Parquet, JPEG XL, WOFF2, PDF, archives and firmware images.
- Vulnerability trigger. The stream encodes an invalid block switch. The fast path ignores the failure, the decoder waits for “more input,” and attacker bytes overrun the eight-byte buffer into neighboring decoder state.
- Execution impact. Memory disclosure or a crash at minimum. Imperva’s red team ran attacker-supplied code on an unmodified Node.js 24.22.0 Linux arm64 build, delivered both as a fetch() response and as an Express request body. It took several attempts, has not been released, and was not tested on other builds. For Python, corruption is confirmed under AddressSanitizer; no exploitation attempt has been published.
This article stays at the root-cause level on purpose. Exploit primitives beyond that are left out.

Disclosure timeline
The fix sat in public source for eight months before anyone labeled it a security issue. The CVE record dates public disclosure to 27 February 2026 for that reason, and every project carrying its own copy is now shipping late.
| Date (2026) | Event |
|---|---|
| 27 Feb | Upstream commit 0ed4268 lands without a security label |
| 21 Oct | Node.js ships 22.23.4, 24.22.1, 26.12.1 (High). Google tags Brotli 1.0.10, 1.1.1, 1.2.1. Vercel deploys inbound WAF rules. “Node2Shell” appears |
| 22 Oct | CVE published by Google; GHSA-9v8h-qqgm-4m35 published; PyPI Brotli 1.2.1 released; Bun 1.4.4 ships; Imperva publishes RCE research. Amazon Linux 2023 lists brotli and nodejs24 as pending |
| 23 Oct | brotlicffi 1.2.1.0 on PyPI; CISA SSVC assessment; PYSEC-2026-4322 and -4323 added; GHSA review maps both PyPI packages; Envoy GHSA-3wvj-j6qm-62mr; Debian trixie fixed in 1.1.0-2+deb13u1; Go port fixed; the name “HTTP2Shell” appears |
| 24 Oct | Bun adopts the HTTP2Shell name. More than a dozen vendored copies still unpatched |
Two corrections to circulating coverage. OpenAI did not find the bug in February; the root cause was fixed then without being recognized, and OpenAI found the vulnerability during a recent red-teaming exercise. Python also issued no advisory of its own; the Python impact comes from OpenSourceMalware and the PyPA PYSEC records.
Affected Versions
Fixed
| Package | Vulnerable Versions | Fixed Version | Notes |
|---|---|---|---|
| Google Brotli (C) | 1.0.3 to 1.0.9, 1.1.0, 1.2.0 | 1.0.10, 1.1.1, 1.2.1 | CVE record ranges; source of every other copy |
| Node.js 22.x | < 22.23.4 | 22.23.4 | Bundled copy; OS patch does not fix official binaries or Docker images |
| Node.js 24.x | < 24.22.1 | 24.22.1 | Imperva RCE shown on 24.22.0 arm64 |
| Node.js 26.x | < 26.12.1 | 26.12.1 | |
| Node.js 18.x, 20.x, 23.x, 25.x | All | None | End of life |
| Bun | 1.4.3 and earlier | 1.4.4 | Rebuild bun build –compile executables |
| PyPI Brotli | 1.0.4, 1.0.6, 1.0.7, 1.0.8, 1.0.9, 1.1.0, 1.2.0 | 1.2.1 | PYSEC-2026-4322; released versions in range per PyPI |
| PyPI brotlicffi | 1.0.9.0 to 1.0.9.2, 1.1.0.0, 1.2.0.0 to 1.2.0.2 | 1.2.1.0 | PYSEC-2026-4323 |
| Envoy | 1.39.2, 1.38.5, 1.37.7, 1.36.11 and earlier | 1.40.0, 1.39.3, 1.38.6, 1.37.8, 1.36.12 | Only with the Brotli decompressor configured |
| Istio | 1.31.0 to 1.31.1, 1.30.0 to 1.30.5, 1.29.0 to 1.29.8 | 1.31.2, 1.30.6, 1.29.9 | Only with Brotli decompression enabled; 1.29 is end of life |
| cdxgen standalone binaries and images | 13.4.0 and earlier, 12.8.8 and earlier | 13.5.0, 12.8.9 | npm install uses system Node instead |
| Debian brotli | bookworm, trixie | 1.0.9-2+deb12u1, 1.1.0-2+deb13u1 | Backports keep the old upstream version string; sid unfixed |
| Alpine 3.24 brotli | < 1.2.1-r0 | 1.2.1-r0 | |
| Go andybalholm/brotli | Before 23 Oct fix | Fixed 23 Oct | Crash only |
| Chromium, Firefox, ngx_brotli, Brotli4j, Ghostscript, SumatraPDF | Vendor-specific | Reported fixed | Electron apps carry Chromium and Node copies |
Not fixed yet
| Package | Vulnerable Versions | Fixed Version | Notes |
|---|---|---|---|
| .NET runtime | Bundles 1.1.0 | None | BrotliDecoder, BrotliStream, HttpClient auto-decompression |
| PHP brotli extension | Current | Pending (PR #105) | |
| Ruby brotli gem | Current | Pending (issue #64) | |
| DuckDB, ClickHouse | Current | Pending (PR #27791, PR #127419) | Parquet input |
| Apache Arrow bundled build | Bundles 1.0.9 | None | Parquet input |
| libjxl, MuPDF, NanaZip, 7-Zip-zstd, p7zip | Current | None | File-format input |
| EDK II, UEFITool | Current | None | Firmware images |
| GraalJS / v8-jsi, YDB | Current | None | |
| Debian nodejs, Amazon Linux 2023 brotli and nodejs24 | Current | Pending | Distro packages lag upstream |
Pure Java, JavaScript and C# ports (pdf.js, iText, HtmlUnit) are not affected.
Exposure Analysis
| Environment | Risk Level | Reason |
|---|---|---|
| Internet-facing Node.js APIs on Express 5 / body-parser 2.x | Critical | express.json() inflates br bodies by default; unauthenticated reach |
| Services fetching attacker-chosen URLs (webhooks, link previews, URL import, image proxies, OIDC discovery) | Critical | Hostile server returns a br response; inbound WAF rules never see it |
| Browsing AI agents and crawlers | Critical | Every fetched page is attacker input to an auto-decoding client |
| Envoy / Istio with the Brotli decompressor | High | Proxy decodes before application controls apply |
| CI/CD runners, build tools, SBOM generators (cdxgen binaries) | High | Scan and download untrusted input; runners hold tokens and signing keys |
| Data pipelines (DuckDB, Arrow, ClickHouse) on third-party Parquet | High | File-borne trigger, no fix yet |
| .NET services with HttpClient auto-decompression | High | No fix yet |
| Developer workstations, Electron apps | Medium | Two embedded copies per Electron app; local secrets |
| Apache / Nginx default configs | Low | Encoder only; request bodies not decoded by default |
The Python exposure has a trap built in. Earlier urllib3 2.6.x decompression fixes told users to move to at least Brotli 1.2.0 or brotlicffi 1.2.0.0, and the urllib3[brotli] extra enforced those floors. Teams that followed that guidance landed on the last vulnerable releases. Any environment with either package installed, directly or through an extra, decodes br responses in requests and aiohttp automatically.
Real-World Impact
No in-the-wild exploitation has been reported. CISA’s SSVC entry on 23 October records exploitation as none and automatable as no, with total technical impact, and Red Hat reports no reliable public PoC for code execution. Published PoC code reportedly reaches code execution only against deliberately weakened builds. That gap will close: the patch diff is public, the trigger condition is small, and downstream release notes already describe reachable paths in plain language. Vendure states its Shop API accepts Brotli request bodies, so an unauthenticated request reaches the vulnerable code. Bulwark webmail describes a signed-in user pointing the server at a calendar or WebDAV host that returns a crafted response.
The realistic scenarios follow the outbound path. An attacker registers a webhook or submits a URL, the backend fetches it, the response decodes, and the attacker lands in a process that usually holds cloud credentials, database secrets and service tokens. CI runners and SBOM generators that scan untrusted artifacts sit in the same position with higher-value secrets. Inbound, any Express 5 API that accepts JSON is reachable with one POST. Memory disclosure is the lower bar; leaked heap from a shared process can expose other users’ request data.
Scale comes from install base, not telemetry. Node.js and requests sit under most server-side web code, and the vendored list covers browsers, the .NET runtime, data warehouses and firmware tooling. Phoenix is not quoting a Shodan figure, because exposure depends on decoder configuration and fetch behavior that banner scans cannot see.
Detection Guidance
Log indicators
| Signal | Where | Meaning |
|---|---|---|
| Content-Encoding containing br on requests (any case, including gzip, br) | Edge proxy, WAF, API gateway logs | Inbound attempts; legitimate clients rarely send Brotli bodies to JSON APIs |
| BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH or error -17 | Application logs on patched Node.js and Brotli 1.2.1 | Malformed block switch rejected; likely a probe |
| SIGSEGV / SIGABRT or heap-corruption aborts in node, bun, python, envoy | Container restarts, journald, crash reporters | Possible trigger on an unpatched host, especially after a br request or outbound fetch |
| Outbound fetches to new or user-supplied hosts answering Content-Encoding: br | Egress proxy logs | Outbound path; correlate with webhook, preview and import features |
| Parquet, JPEG XL, WOFF2 or PDF uploads followed by worker crashes | Ingestion logs | File-borne trigger against unpatched tooling |

Scanner coverage
Do not trust early-week scan results. The CVE record names only github.com/google/brotli, and reporting on 24 October said the GHSA still listed no affected package. The GHSA page has since been reviewed and maps brotli >=1.0.3,<1.2.1 and brotlicffi >=1.0.3,<1.2.1.0, and OSV carries the PYSEC entries. Re-run pip-audit, Dependabot, Safety and Phoenix SCA ingestion, then confirm by hand that findings appear.
Manifest scanning still misses most copies. The decoder inside Node.js, Bun, Envoy, .NET, cdxgen binaries and wheels never appears in a lockfile. Distro backports cut the other way: Debian’s fixed 1.0.9-2+deb12u1 still reads as 1.0.9, so naive version matching yields false positives. Node.js cherry-picked the fix, so judge Node by the Node version, not process.versions.brotli.
Advisory data for HTTP2Shell changed daily this week, and it will keep changing as vendored copies get fixed. Phoenix Blue, Phoenix Security’s free threat intelligence feed, follows issues like this one as they move: from a quiet upstream commit to a published CVE, from one runtime to every product that vendors the same code, and from claimed RCE to demonstrated RCE. Sign up free at phxintel.security.
Protect yourself with the latest threat intelligence, get free access to Phoenix Blue Today
Verification steps for teams
- Inventory runtimes, including base images: docker run –rm <image> node -v and bun –version. Below 22.23.4, 24.22.1, 26.12.1 or 1.4.4 is vulnerable.
- Check Python lockfiles and live environments: grep -RniE ‘^(brotli|brotlicffi)[=<>~ ]’ requirements*.txt poetry.lock uv.lock Pipfile.lock and python -c “import brotli; print(brotli.__version__)”, repeated for brotlicffi. Look for extras such as urllib3[brotli], httpx[brotli] and aiohttp[speedups].
- Find vendored native copies: find / -name ‘libbrotlidec*’ 2>/dev/null, plus strings -a <binary> | grep -m1 BrotliDecoderDecompressStream across build artifacts, Electron bundles, cdxgen binaries and site-packages.
- Search proxy config for envoy.compression.brotli.decompressor, including Istio EnvoyFilter manifests.
- Map every outbound fetch feature to its runtime and HTTP client and mark it reachable or not.
- Regenerate SBOMs from built images rather than source manifests and diff them against the tables above.
Remediation Guidance
Immediate actions
- Upgrade Node.js to 22.23.4, 24.22.1 or 26.12.1 and Bun to 1.4.4. Rebuild and redeploy every image and compiled executable; a running process keeps the old decoder until it restarts.
- Upgrade Python: pip install -U “Brotli>=1.2.1” “brotlicffi>=1.2.1.0”, pin those floors in lockfiles and constraints files, and restart long-running workers.
- Upgrade system libbrotli where Node, libcurl or Python link it dynamically (Debian nodejs, libcurl on Debian 12 and Ubuntu 24.04), then restart the processes that loaded it.
- Upgrade Envoy to 1.36.12, 1.37.8, 1.38.6, 1.39.3 or 1.40.0 and Istio to 1.30.6 or 1.31.2 where the Brotli decompressor is on. Plan the move off Istio 1.29.
- Upgrade cdxgen standalone binaries and images to 12.8.9 or 13.5.0.
- Move workloads off Node.js 18.x, 20.x, 23.x and 25.x.
- Rotate credentials held by any internet-facing process that ran a vulnerable decoder on untrusted br input. Memory disclosure leaves no clean trail.
Temporary mitigations
Inbound, reject requests whose Content-Encoding contains br before any proxy or body parser decodes them, on direct-to-origin routes as well as the CDN. In Express, put a guard ahead of every parser or set inflate: false:
app.use((req, res, next) => {
const ce = (req.headers['content-encoding'] || '').toLowerCase();
if (ce.split(',').some(v => v.trim() === 'br')) return res.status(415).end();
next();
});
Outbound, dropping br from Accept-Encoding does not help, because a server can send Brotli regardless. Disable automatic decoding for untrusted fetches and check Content-Encoding before reading the body; in urllib3, use preload_content=False and read with decode_content=False. Node.js fetch() has no published workaround, so the runtime upgrade is the only fix there.
For Python services where Brotli is optional, uninstall both Brotli and brotlicffi and drop extras such as httpx[brotli]; clients fall back to gzip and deflate. For Next.js, images.unoptimized stops the optimizer fetching and decoding remote images. For Envoy and Istio, remove the Brotli decompressor or disable both its directions, and leave gzip and zstd entries that share the envoy.filters.http.decompressor name. For .NET, disable Brotli in HttpClientHandler.AutomaticDecompression on clients that fetch untrusted URLs. Quarantine third-party Parquet with Brotli-compressed columns until DuckDB, Arrow and ClickHouse ship fixes. Vercel and Azure WAF rules cover inbound traffic only.
Phoenix Security Recommendations
HTTP2Shell is a vendored-library problem first, so the work is separating “the advisory applies to this artifact” from “this decoder is reachable with untrusted input in production.”
Contextual deduplication collapses the same CVE reported by SCA on requirements.txt, by container scanning on the Node base image and by the distro scanner on libbrotli into one finding per asset. Without it, one Node service can surface as three tickets with three owners and three version strings, one of them a Debian backport false positive.
Binary and SBOM evidence closes the manifest gap. Phoenix ingests image-level SBOMs and runtime package evidence, so a Node 24.22.0 binary, a Bun-compiled executable, a cdxgen binary or a .NET service carrying Brotli 1.1.0 shows up even when no lockfile names Brotli. Mark those as vendored so owners know a dependency bump will not fix them.
Reachability analysis ranks what is left. An internet-facing Express 5 service with express.json(), a webhook worker fetching customer URLs and an Envoy mesh with the Brotli decompressor go first. A batch job with Brotli installed and no untrusted fetches waits. Attack surface management supplies the internet-facing half and ties it to the owning team.
Run it as a remediation campaign:
- Create the HTTP2Shell campaign scoped to CVE-2026-111497, GHSA-9v8h-qqgm-4m35, PYSEC-2026-4322 and PYSEC-2026-4323, with fixed versions as targets.
- Assign owners automatically through vulnerability ownership attribution on repositories, images and services.
- Track fixes per asset with separate states for patched, mitigated (br rejected or auto-decode off) and no vendor fix.
- Verify runtime exposure after redeploy from the running image digest and Node version, not the merged pull request.
Download the campaign config. Import it into Phoenix to find every vulnerable Brotli, brotlicffi and Bun version across your assets.
Phoenix Security correlates vulnerable decoder copies with running workloads, finds internet-exposed and fetch-heavy services through attack surface management, and assigns each to an owner, shrinking a cross-ecosystem CVE into a backlog teams can close.
External References
- CVE record (cvelistV5, with CISA ADP SSVC)
- NVD
- GitHub Advisory GHSA-9v8h-qqgm-4m35
- PyPA PYSEC-2026-4322 (Brotli)
- PyPA PYSEC-2026-4323 (brotlicffi)
- Node.js October 2026 security releases
- Node.js v22.23.4 release notes
- google/brotli commits 0ed4268, 50a9b70, b418fa8, da98063
- google/brotli releases v1.0.10, v1.1.1, v1.2.1
- Envoy advisory GHSA-3wvj-j6qm-62mr
- ISTIO-SECURITY-2026-007
- Debian security tracker
- OpenSourceMalware, Node2Shell affects Python
- Imperva, Node2Shell from Brotli data to RCE
- The Hacker News, Brotli decoder flaw also affects Bun, Python and other software (24 Oct 2026)
- Patch the Planet
- Broccoli2Shell vendored-copy tracker
- Vendure, October 2026 Node.js releases
- Microsoft, Azure WAF for CVE-2026-111497















