Content-Encoding: br with Brotli 1.0.3 to 1.2.0 is exposed: Node.js, Bun, Envoy, Python Brotli/brotlicffi, .NET, Electron. Check Fleet’s Software page first, then run the six queries. Download all of them as one file: http2shell-fleet-queries.sql.What it is#
The bug lives in c/dec/decode.c of Google’s Brotli library. When the decoder hits a block switch it cannot complete, vulnerable versions keep decoding instead of stopping. A crafted stream then writes past a small buffer into neighbouring decoder state. The upstream fix (google/brotli commit 0ed4268) makes it stop on the first format error.
Two ways in, both unauthenticated:
- Inbound. A server decompresses a request body sent with
Content-Encoding: br. Express 5 does this by default throughexpress.json(). - Outbound. An app fetches a Brotli response from a server the attacker controls. Node.js
fetch(), Pythonrequests/httpx/aiohttp(whenBrotliorbrotlicffiis installed) decode it automatically.
Imperva reported code execution on the stock Node.js 24.22.0 Linux arm64 image. Other researchers got crashes. Not in CISA KEV as of 26 October.

Fixed versions#
| Component | Vulnerable | Fixed |
|---|---|---|
| Google Brotli | 1.0.3 to 1.2.0 | 1.0.10, 1.1.1, 1.2.1 |
| Node.js | 22.x, 24.x, 26.x before fix; 20.x and older, 23.x, 25.x are EOL | 22.23.4, 24.22.1, 26.12.1 |
| Bun | 1.0.21 to 1.4.3 | 1.4.4 |
PyPI Brotli / brotlicffi | 1.0.3 to 1.2.0 / up to 1.2.0.x | 1.2.1 / 1.2.1.0 |
| Envoy / Istio | Brotli decompressor enabled | 1.40.0, 1.39.3, 1.38.6, 1.37.8 / 1.31.2, 1.30.6 |
| Chrome, Edge, Firefox, Electron | pre-148 / pre-150 / Electron 41 and older | 148+, 150 (ESR 140.10, 115.35), Electron 42+ |
| Ubuntu, RHEL, SUSE, AL2023, Fedora 43 to 45 brotli; Ubuntu/RHEL nodejs; Kibana; .NET | all | no fix yet as of 24 October |
Two traps. A patched Node.js still reports process.versions.brotli as 1.1.0 or 1.2.0, so never grade Node by its Brotli string. Distros backport without bumping the upstream number (Debian 12 is fixed in brotli 1.0.9-2+deb12u1), so grade distro packages against the vendor advisory, not the version.
Step 0: Fleet already knows most of it#
Fleet inventories deb_packages, rpm_packages, homebrew_packages, apps, programs, python_packages and npm_packages every hour. Open Software, search brotli, node, bun, envoy. Open Vulnerabilities, search CVE-2026-111497; the match lands once NVD publishes CPE configuration for it.

The queries exist for what inventory misses: nvm, Volta and fnm binaries, macOS .pkg installs, bundled DLLs, wheels that vendor libbrotli, and which of those copies is actually reachable from the network.
Detect: six Fleet queries#
Every query is in http2shell-fleet-queries.sql. Scope each to the platform named in its heading.
Why the version logic looks odd. SQLite compares version strings as text: '1.0.9' < '1.0.10' is false, '24.9.0' > '24.22.1' is true. The queries never use < or > on a version. Brotli has exactly nine vulnerable releases, so it is an IN list. Node.js and Bun are split into major, minor and patch with CAST, which keeps the leading digits ('22.1-1nodesource1' casts to 22), and compared as a number. A Node.js major newer than 26 returns REVIEW, not FIXED, so a future vulnerable line cannot pass the policy silently.
1. Linux: brotli and nodejs packages#
WITH pk AS (
SELECT name, version, 'deb' AS src FROM deb_packages
WHERE name LIKE '%brotli%' OR name = 'nodejs'
UNION ALL
SELECT name, version || '-' || release, 'rpm' FROM rpm_packages
WHERE name LIKE '%brotli%' OR name = 'nodejs'
),
k AS (
SELECT *, CAST(version AS INT) AS maj,
CAST(substr(version, instr(version,'.')+1) AS INT) AS mnr,
CAST(substr(substr(version, instr(version,'.')+1), instr(substr(version, instr(version,'.')+1),'.')+1) AS INT) AS pat
FROM pk
)
SELECT name, version, src,
CASE WHEN name <> 'nodejs' THEN 'REVIEW-vs-advisory'
WHEN maj>26 THEN 'REVIEW'
WHEN (maj=22 AND mnr*1000+pat>=23004) OR (maj=24 AND mnr*1000+pat>=22001)
OR (maj=26 AND mnr*1000+pat>=12001) THEN 'FIXED'
ELSE 'VULNERABLE' END AS status
FROM k;deb_packages.version already carries the Debian revision; rpm_packages keeps release separate, so it is joined on.
2. macOS: Homebrew and Node.js .pkg receipts#
WITH src AS (
SELECT name, version, path AS location FROM homebrew_packages
WHERE name = 'brotli' OR name LIKE 'node%' OR name IN ('bun','envoy')
UNION ALL
SELECT package_id, version, location FROM package_receipts
WHERE package_id LIKE 'org.nodejs.node%'
),
k AS (
SELECT *, ltrim(version,'v') AS v FROM src
),
n AS (
SELECT name, version, location, CAST(v AS INT) AS maj,
CAST(substr(v, instr(v,'.')+1) AS INT) AS mnr,
CAST(substr(substr(v, instr(v,'.')+1), instr(substr(v, instr(v,'.')+1),'.')+1) AS INT) AS pat
FROM k
)
SELECT name, version, location,
CASE WHEN name = 'brotli' AND version IN ('1.0.3','1.0.4','1.0.5','1.0.6','1.0.7','1.0.8','1.0.9','1.1.0','1.2.0') THEN 'VULNERABLE'
WHEN name = 'brotli' THEN 'FIXED'
WHEN name = 'bun' AND maj*1000000+mnr*1000+pat BETWEEN 1000021 AND 1004003 THEN 'VULNERABLE'
WHEN name = 'bun' THEN 'FIXED'
WHEN name = 'envoy' THEN 'REVIEW'
WHEN maj>26 THEN 'REVIEW'
WHEN (maj=22 AND mnr*1000+pat>=23004) OR (maj=24 AND mnr*1000+pat>=22001)
OR (maj=26 AND mnr*1000+pat>=12001) THEN 'FIXED'
ELSE 'VULNERABLE' END AS status
FROM n;package_receipts catches the installer from nodejs.org (org.nodejs.node.pkg), which no package manager table sees.
3. All platforms: Python Brotli wheels, per user#
SELECT u.username, p.name, p.version, p.path,
CASE
WHEN lower(p.name) = 'brotli' AND p.version IN ('1.0.3','1.0.4','1.0.5','1.0.6','1.0.7','1.0.8','1.0.9','1.1.0','1.2.0') THEN 'VULNERABLE'
WHEN lower(p.name) = 'brotlicffi' AND substr(p.version,1,5) IN ('1.0.3','1.0.4','1.0.5','1.0.6','1.0.7','1.0.8','1.0.9','1.1.0','1.2.0') THEN 'VULNERABLE'
WHEN lower(p.name) = 'brotlipy' THEN 'REVIEW'
ELSE 'FIXED' END AS status
FROM users u CROSS JOIN python_packages p USING (uid)
WHERE lower(p.name) IN ('brotli','brotlicffi','brotlipy');The CROSS JOIN users ... USING (uid) is what makes python_packages walk every user’s site-packages and venvs instead of only the system interpreter.
4. macOS/Linux: Node and Bun outside package managers#
SELECT path, symlink_target_path, size, datetime(mtime,'unixepoch') AS modified FROM file
WHERE path IN ('/usr/bin/node','/usr/local/bin/node','/opt/homebrew/bin/node','/usr/local/bin/bun','/home/linuxbrew/.linuxbrew/bin/node')
OR path LIKE '/Users/%/.nvm/versions/node/%/bin/node'
OR path LIKE '/home/%/.nvm/versions/node/%/bin/node'
OR path LIKE '/Users/%/.volta/tools/image/node/%/bin/node'
OR path LIKE '/home/%/.volta/tools/image/node/%/bin/node'
OR path LIKE '/Users/%/.local/share/fnm/node-versions/%/installation/bin/node'
OR path LIKE '/home/%/.local/share/fnm/node-versions/%/installation/bin/node'
OR path LIKE '/Users/%/.fnm/node-versions/%/installation/bin/node'
OR path LIKE '/home/%/.fnm/node-versions/%/installation/bin/node'
OR path LIKE '/Users/%/Library/Application Support/fnm/node-versions/%/installation/bin/node'
OR path LIKE '/Users/%/.bun/bin/bun'
OR path LIKE '/home/%/.bun/bin/bun';The Node version is in the folder name for nvm, Volta and fnm. fnm defaults to ~/.local/share/fnm on macOS and Linux, falls back to ~/.fnm for old installs, and still reads ~/Library/Application Support/fnm on Macs that installed it before the switch (fnm source). A custom FNM_DIR or XDG_DATA_HOME is not covered. For /usr/local/bin/node, symlink_target_path usually points into one of those folders and gives the version for free.
5. Windows: Node.js, Bun and the .NET native DLL#
SELECT 'program' AS src, name, version, install_location AS path FROM programs
WHERE name LIKE 'Node.js%' OR name LIKE '%Bun%'
UNION ALL
SELECT 'file', filename, product_version, path FROM file
WHERE path = 'C:\Program Files\nodejs\node.exe'
OR path LIKE 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\%\System.IO.Compression.Native.dll'
OR path LIKE 'C:\Users\%\.bun\bin\bun.exe'
OR path LIKE 'C:\Users\%\AppData\Roaming\nvm\%\node.exe';The native DLL is the right file. The managed System.IO.Compression.Brotli code is a thin wrapper that calls into System.IO.Compression.Native, which compiles in the Brotli C decoder (dotnet/runtime interop). .NET has no vendor statement yet. Inventory it now so the day Microsoft ships a fix you already know where it goes.
6. All platforms: who is actually reachable#
WITH lp AS (
SELECT pid, group_concat(DISTINCT port) AS ports, group_concat(DISTINCT address) AS addresses
FROM listening_ports
WHERE address NOT IN ('127.0.0.1','127.0.1.1','::1','') AND port > 0
GROUP BY pid
),
hits AS (
SELECT lp.pid, p.name, p.path, lp.ports, lp.addresses,
(SELECT m.path FROM process_memory_map m WHERE m.pid = lp.pid AND m.path LIKE '%brotli%' LIMIT 1) AS brotli_lib
FROM lp JOIN processes p USING (pid)
)
SELECT pid, name, path, ports, addresses, brotli_lib FROM hits
WHERE lower(name) IN ('node','node.exe','bun','bun.exe','envoy','envoy.exe','dotnet','dotnet.exe','nginx','java')
OR brotli_lib IS NOT NULL;Three things make this cheap enough to run fleet-wide. process_memory_map is only read for pids that are listening, never for every process. Ports are grouped first, so a process on 40 ports is one row and its memory map is read once, not 40 times. And the brotli lookup is a LIMIT 1 subquery, so it stops at the first hit. Statically linked decoders (Node, Bun) never show up as a library; that is what queries 1 to 5 are for.

What it found on 33 hosts. The vLLM worker processes on one host had pillow.libs/libbrotlicommon-53534446.so.1.2.0 mapped. The Pillow wheel vendors its own libbrotli 1.2.0. No package named brotli on that host, no python_packages row for it, no Fleet inventory hit. Only the memory map shows it. The Java rows (Wazuh indexer and other JVM services) are listed for review because Netty stacks can pull in brotli4j (fixed in 1.24.0). Nginx is listed because ngx_brotli links libbrotlidec; the empty brotli_lib column says this build does not.
Policy#
Wrap any status query so the host fails while a VULNERABLE row exists. One policy per platform, same shape:
SELECT 1 FROM os_version WHERE NOT EXISTS (
SELECT 1 FROM ( <query 1, 2 or 3 here> ) WHERE status = 'VULNERABLE'
);Fix#
- Update OS packages where a fix exists, then restart the processes still holding the old library (query 6 shows which).
- Update owner-managed copies: nvm, fnm, Volta, Homebrew, Python lockfiles, Bun executables, container images.
bun build --compilebinaries must be rebuilt. - Until then: reject
Content-Encoding: brat the WAF or ingress; in Express 5 setexpress.json({ inflate: false })(also onurlencoded(),text(),raw()); remove the Brotli decompressor filter from Envoy and Istio; uninstallBrotli/brotlicffifrom Python environments that only need it for outbound fetches.