On the back of Imperva's disclosure of a critical remote-code-execution bug in the Brotli decoder (CVE-2026-111497, "Node2Shell"), the obvious question for a defender is not “is Brotli vulnerable” — it is “which of the packages I pull in quietly carry their own compiled copy of the decoder — and which only look like they do.” That is the question our file-level corpus can answer, so we did.
The vulnerability (credit: Imperva)
Imperva security researcher Ron Masas (now Thales Security) discovered an out-of-bounds write in the Brotli decoder's C implementation: crafted compressed data can corrupt heap memory and lead to remote code execution, rated CVSS 9.2. It affects the C library 1.0.3 through 1.2.0, fixed in 1.0.10, 1.1.1 and 1.2.1. Because Node.js bundles Brotli, every Node runtime decoding attacker-supplied compressed data is a potential shell — hence Node2Shell. Microsoft's .NET advisory (GHSA-p3jp-9v83-76w9) is the same bug in another runtime. The discovery, root cause and severity are entirely the researchers' work; our contribution is strictly the package-level exposure mapping below.
Brotli decodes a large share of the compressed web, so it turns up server-side in HTTP stacks, upload pipelines and asset tooling far more often than people expect — which is exactly why knowing which package ships a decoder copy matters.
Bundle vs. link — and why the package version lies
Two things had to be right for this to be useful. First, the bundle-vs-link distinction: language bindings that link the operating system's libbrotli (typical Ruby gems and Python wheels) carry the vulnerability on the host, not inside the package — patch the OS library and you are done. The packages that matter here are the ones that bundle their own copy: vendored C sources compiled at install time, a prebuilt native binary, or a WASM build of the decoder. That copy travels with the package and ignores your system updates.
Second — and this is the trap — the package's own version number routinely does not match the Brotli version inside it. In our data, one RubyGems wrapper line ships a vulnerable 1.2.0 in one release and a patched 1.2.1 in the next, under near-identical wrapper versions. A naive name-and-version scan cannot tell these apart. The only reliable method is to open each archived artifact and read the real bundled version. Everything below is classified on the vendored C version, never the package version.
What we found
Across our published-package corpus, these are the ecosystems where packages carry a real C decoder copy — verified at the file level. Vulnerable = bundled Brotli falls in 1.0.3–1.2.0; patched = ≥ 1.0.10 / 1.1.1 / 1.2.1. Package identities are withheld here; the full list with versions and evidence is shared with researchers on request.
The twist that defines this CVE: on npm — the ecosystem the headlines are about — every Brotli-named package we archived is a pure JavaScript or WASM port. No C sources, no out-of-bounds write, not this bug. The actual vulnerable copies live in RubyGems and crates.io, in native-extension gems and -sys crates that headline-level scanners routinely skip. And PyPI's official bindings are not in our corpus yet — a coverage gap we are closing.
What the corpus sees
A question like this is answerable at three speeds. The slowest is what we described above: open each archived artifact and read the bundled version header. But the corpus carries two faster layers beneath it. Every package we have ever scanned carries its full file listing — the paths inside the artifact, indexed and queryable — so “who ships a brotli/c/dec/ tree” is a database query, not an excavation. And the dependency graph is queryable too, so the blast radius is arithmetic.
Coverage matters more than size here. Of the ecosystems in this report, our file listings reach 91% of PyPI, 88% of crates.io and 93% of RubyGems — the three places the vulnerable copies turned out to live. npm sits at 83%, Go at 43% and Chrome extensions at 29% (the two we are still closing). The PyPI gap in the table above is a package-level gap, not a visibility gap: had the official bindings been published to PyPI in our window, the file listing alone would have flagged them — no archive excavation required.
The honest limits: the dependency graph is npm-complete but thinner elsewhere, so the transitive blast radius is a npm-only figure for now; and a file listing proves a package carries Brotli, not which version — that last step still belongs to the artifact. Two of the four vulnerable artifacts we report were confirmed that way, from version.h inside the gem.
Node is the one that matters
The dominant attack surface is not a package at all — it is Node.js itself. The npm node runtime package ships prebuilt Node binaries as a dependency, and of the 13 versions in our corpus, at least 8 predate the security fixes (22.23.4 / 24.22.1 / 26.12.1). 115 packages declare it as a direct dependency; anything resolving a pre-fix runtime through it is the npm-side exposure to Node2Shell. Lockfiles pin the runtime independently of the top-level version — check the one that actually resolved.
One quirk worth knowing: patched Node builds still report Brotli 1.2.0 in process.versions — the fix was backported without bumping the bundled library's version string. Trust the Node version, not the bundled-library string.
The dependency blast radius, in numbers
Roughly 140 npm packages declare a Brotli wrapper directly. The runtime-only transitive closure reaches 1,155 package names — 14,083 versioned artifacts with ≈20 million recorded weekly downloads. Including dev-dependencies the closure balloons to 306,235 packages, but that number is mostly build-time tooling (Webpack plugins, size reporters) where the decoder never runs in production — which is why the runtime-only figure is the one that counts.
The timeline is the part that should worry defenders: 5,792 affected artifacts were published before the advisory — a silent stock of exposure — and 350 more after it, including packages that newly pinned Brotli dependencies in the days since the disclosure. Exposure keeps growing after the CVE is public.
The bait wave
Every headline CVE pulls a wave of typosquats riding its name, and this one is no exception: our triage already holds three Brotli-themed malicious packages (all verdicted malicious — install-time credential theft and decoy payloads, not decoder exploits). Since the advisory on 29 October, no new Brotli-named package has entered our npm intake yet — the wave is late this time, and we are watching for it. Identities are withheld here and shared with researchers on request.
What to do about it
- If you run Node servers: upgrade to ≥ 22.23.4 / 24.22.1 / 26.12.1. Trust the Node version — patched builds still print Brotli 1.2.0 in
process.versions. - If you pull native extensions from RubyGems or crates.io: verify the bundled C version (the
version.hheader inside the artifact), not the package version. We found 4 artifacts in the vulnerable range across the two ecosystems. - If you depend on Brotli-named npm packages: almost certainly JS/WASM ports — not this bug. Verify anyway; none of the ones we archived carry C code.
- Treat compressed payloads as untrusted input and decode at the edge where you can afford to drop the connection.
- The full affected-package list — with versions and vendored-version evidence — is available to researchers on request.
Exposure mapping is database-first: the dependency graph (4.27M npm edges) and per-package file listings (≈91% of PyPI, 88% of crates.io, 93% of RubyGems, 83% of npm) answer the “who” and “how many”; opening the archived artifact to read the vendored version.h — or the crate's documented upstream version — is the final verification, never inferred from the package name or version. Static inspection only; we never install packages. Packages that only link the system library are excluded — they do not carry a copy. The vulnerability was discovered and disclosed by Ron Masas (Imperva / Thales Security); OSV aliases include CVE-2026-111497, GHSA-p3jp-9v83-76w9 (.NET), GHSA-9v8h-qqgm-4m35 (PyPI) and BIT-node-2026-111497 (Node runtime images).