CVE-2026-111497, the Brotli decoder bug that Node.js patched on 21 October, entered CISA’s Known Exploited Vulnerabilities catalog on 27 October, after public proofs of concept, two independent code-execution demos and exploit traffic seen in the wild. It has also stopped being a Node.js story: the flaw lives in Google’s Brotli library, so Python packages, .NET, Bun, Envoy and Istio carry it too, and Node.js 20, 23 and 25 will never get a fix.
In my 21 October security-day roundup I put Node.js at ‘48 h (today if you decompress untrusted Brotli)’. A week later that call is out of date. This post covers what changed, what it means for teams that ship code, and the work for this week: a read-only inventory, patch steps per environment and mitigations that hold until you can patch. Every version and command below comes from vendor advisories, project docs or my own checks of source trees and registries on 28 October.
1. Context: From a Node.js Line Item to a KEV Entry
The Node.js advisory describes the bug plainly: an invalid block switch was reported as a temporary need for more input instead of a permanent format error, so the streaming decoder kept appending bytes to an eight-byte internal buffer. Anyone who can make your process decompress crafted Brotli data can corrupt its memory, with outcomes ranging from a crash to disclosure of process memory or remote code execution. Node rates it High and credits OpenAI Codex Security (Khai Tran and Sylvie Mayer) for the report. Google, as the CNA, scores it CVSS v4.0 9.2 Critical in the CVE record, which covers Brotli 1.0.3 through 1.2.0 on every platform.
What changed after 21 October is the threat picture. CISA added it to the KEV catalog on 27 October as ‘Google Brotli Unchecked Return Value Vulnerability’, with a 10 November due date for US federal agencies. The day before, ELLIO recorded a single exploit request against Node.js 24.22.0 on x86-64, from an IP it had already seen throwing PHP2Shell exploits. The bug collected names along the way: Node2Shell, Brotli2Shell, HTTP2Shell and Broccoli2Shell are all the same CVE.
Timeline (dates as published by each source)
How each source rates it
| Source | Rating | What to read into it |
|---|---|---|
| Google (CNA, CVE record) | CVSS v4.0 9.2, Critical | AT:P: the attack needs a precondition, a path that decodes untrusted Brotli |
| Node.js | High | No CVSS in the advisory, and no workaround offered |
| Microsoft (.NET) | CVSS v3.1 8.1 | AC:H; .NET is exposed only when an app opts into Brotli decompression |
| Red Hat | Important, CVSS v3.1 8.1 | RHSB-2026-015 lists RHEL 7 to 10; errata still in progress on 27 October |
| Ubuntu | Medium priority | Its nodejs packages link the system libbrotli and are marked not affected |
| CISA KEV | Added 27 Oct, due 10 Nov | Listed as exploited; ransomware use ‘Unknown’ |
The fix was public for eight months
The upstream commit landed on 27 February with a one-line message that says nothing about security, and sat in a public repository until the 21 October releases. Anyone diffing decode.c could have found it. Node.js patched its bundled copy in place rather than jumping to a new Brotli release, which matters for how you verify the fix (step 1 below).
Where things stand (checked 28 October 2026)
Fixed versions
The Node.js, Brotli, Python, .NET and Bun versions come from nodejs.org, the CVE record, PyPI, Microsoft’s advisory and Bun’s release. I checked the Node.js source tags myself.
KEV listing
CISA’s catalog entry, which I read in its JSON feed: added 27 October, due 10 November, CWE-252 and CWE-787.
Exploitation in the wild
ELLIO reports one exploit request from 94.154.43.7 on 26 October; Tenable says it has not independently confirmed it, and ELLIO’s own tests of that chain got zero shells in 50 attempts. CISA does not publish its evidence. I found no confirmed successful compromise, which is absence of evidence, not evidence of absence.
Code execution
Shown in labs only: Imperva on arm64 after several attempts, ELLIO on x86-64 at around the fifth attempt with ASLR on. Both chains are tied to exact builds.
Downstream fixes still missing
As of 26 October, Tenable lists no fixed ClickHouse, Apache Arrow or FFmpeg release. Debian’s tracker still marks its nodejs packages vulnerable in bookworm and trixie, and Red Hat’s errata are pending.
Golden rule
Count Brotli decoders, not Node.js installs: patch every runtime and package that decodes untrusted br data, restart what loaded it, and verify by version or package revision, never by process.versions.brotli.
2. Five Implications for Teams That Ship Code
The Tenable FAQ is the best single map of the blast radius, and Imperva’s write-up is the clearest account of which application paths actually reach the decoder. Read together, they change how I would prioritise this bug.
The 48-hour window is now a KEV deadline
CISA KEV · Rapid7 · ELLIO
Rapid7 notes that CISA listed the CVE based on evidence of active exploitation. Federal agencies have until 10 November; everyone else should read that as ‘this week’. For any internet-facing service that decodes Brotli in either direction, my urgency moves from 48 hours to today.
Exploits are build-specific, and that cuts both ways
Imperva · ELLIO
ELLIO says the observed chain targets exactly one binary, the official v24.22.0 x86-64 release. That makes failed attempts likely, but ELLIO expects waves, because an attacker measures the offsets once per build and reuses them on every host running it. Failed attempts still crash the worker, and ELLIO warns that core dumps can expose session data.
It is a library bug, so count decoders, not runtimes
Tenable · CVE record · Red Hat
The CVE record covers Brotli itself, not Node.js. Tenable lists Python’s Brotli and brotlicffi, .NET 8 to 11, Bun, Envoy, Istio, PHP’s Windows builds and PHP’s brotli extension, and Red Hat adds Firefox and Thunderbird, which embed their own copy. Updating Node.js fixes Node.js and nothing else.
Both directions count
Imperva · Akamai · Aikido
Inbound, Express 5 and body-parser 2 inflate Brotli request bodies by default, so any client can deliver the payload. Outbound, Node’s fetch(), axios and got decode whatever a server sends, so a webhook, link preview or crawler that calls a URL you don’t control is a target. A bare node:http server does not decode bodies on its own.
End-of-life lines are on their own
HeroDevs · Node.js · .NET
Node.js 20, 23 and 25 get no fix: HeroDevs read the source of the final 20.20.2 and found the vulnerable decoder (Brotli 1.1.0), with fetch() decoding br there too. Debian’s own nodejs packages (18.x in bookworm, 20.x in trixie) were still marked vulnerable when I checked. And .NET 8 and 9 leave support on 10 November, the same day as the KEV deadline.
Beyond Node.js: fixed versions and when you are exposed
| Product | Fixed in | Exposed when |
|---|---|---|
| Google Brotli (C library) | 1.0.10 · 1.1.1 · 1.2.1 | Anything decoding untrusted br with 1.0.3 to 1.2.0 |
| Node.js | 22.23.4 · 24.22.1 · 26.12.1 | Untrusted br reaches node:zlib: body parsers, fetch(), your own calls |
| Python Brotli / brotlicffi | 1.2.1 / 1.2.1.0 | Installed next to urllib3/requests, httpx[brotli] or aiohttp |
| .NET 8 / 9 / 10 / 11 | 8.0.33 · 9.0.22 · 10.0.14 · 11 RC3 | Only when an app opts into Brotli request or response decompression, or calls BrotliStream or BrotliDecoder |
| Bun | 1.4.4 | Versions 1.0.21 to 1.4.3, per Tenable |
| Envoy / Istio | 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 | Only with a Brotli decompressor configured |
| PHP on Windows | 8.2.35-1 · 8.3.36-1 · 8.4.27-1 · 8.5.12-1 | cURL’s automatic decompression enabled (off by default) |
| FFmpeg · ClickHouse · Apache Arrow | No fixed release listed on 26 Oct | JFrog, via Tenable: instruction-pointer control (FFmpeg via libjxl), a server crash (ClickHouse), a DoS (Arrow) |
Where untrusted Brotli reaches the decoder in a Node.js app
| Path | When it decodes | If you can’t patch today |
|---|---|---|
| Express 5 / body-parser 2 (json, raw, text, urlencoded) | By default: inflate is true | inflate: false, or reject br before the parsers or at the proxy |
| Koa with @koa/bodyparser | Listed by ELLIO among its working targets | Reject br at the proxy |
| Bare node:http server | Never on its own | Search your handlers for zlib Brotli calls |
| Built-in fetch() | Whatever Content-Encoding the response carries | Patch; restrict egress for user-supplied URLs |
| axios, got | decompress defaults to true | decompress: false for URLs you don’t control |
| Your own node:zlib calls | zlib.brotliDecompress(), createBrotliDecompress() | Stop passing untrusted data to them (Red Hat’s advice) |
Express 4 uses body-parser 1.x, which inflates only gzip and deflate; Brotli support arrived in body-parser 2.0.0. While you’re in there, body-parser 2.4.0 and 1.20.9 (22 October) also fix CVE-2026-111181, a separate one-request crash with compressed bodies.
3. What to Do This Week
Same order as any runtime bug: find every copy, patch it the way each environment demands, restart what loaded it, and put a mitigation in front of whatever you can’t patch today. Everything in step 1 only reads.
Step 1: Find every decoder
Node.js runtimes: hosts, containers, single executables, your code
Linux commands; on macOS drop the -r from xargs. Run them on hosts, inside images and on CI runners. The SEA fuse string comes from the Node.js docs, and the release-URL grep works on official nodejs.org builds; I tested it against my own binary.
# 1. Every node binary on the host, version managers included
find / -xdev -type f -name node -perm -u+x 2>/dev/null | while read -r b; do
echo "$("$b" -v 2>/dev/null) $b"
done | sort
# fixed: v22.23.4, v24.22.1, v26.12.1 · v20, v23, v25: no fix
# 2. Running containers: what is deployed, not what the tag points to today
docker ps --format '{{.Names}}' | while read -r c; do
echo "$c $(docker exec "$c" node -v 2>/dev/null || echo 'no node')"
done
grep -rn --include='Dockerfile*' 'FROM node:' .
# 3. Single executable apps: the fuse string ends in :1 once a blob is injected
find /opt /usr/local -xdev -type f -perm -u+x -size +40M -print0 2>/dev/null \
| xargs -0 -r grep -l 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2:1'
# official nodejs.org builds, and SEAs built on them, embed their release URL
grep -aoE 'download/release/v[0-9]+\.[0-9]+\.[0-9]+/' ./my-sea-app | sort -u
# 4. Electron apps embed their own Node: look for Electron 45.1.0,
# 44.10.0 or 43.8.4 (Node.js 24.22.1) in each vendor's release notes
# 5. Your code: direct calls to the Brotli decoder
grep -rnE 'createBrotliDecompress|brotliDecompress(Sync)?\(' src/ \
--include='*.js' --include='*.mjs' --include='*.cjs' --include='*.ts'The other copies: Python, the system library, .NET, Bun
The pip commands come from Aikido’s write-up. Compare distro package revisions with USN-8956-1, DSA-6582-1, DLA-4855-1 or your vendor’s notice, not with upstream numbers.
# Python: Brotli and brotlicffi compile their own copy of the decoder
pip show Brotli brotlicffi # fixed: 1.2.1 / 1.2.1.0
docker run --rm <image> pip show Brotli brotlicffi
grep -i brotli poetry.lock uv.lock requirements*.txt
# The system library (Ubuntu and Alpine nodejs packages link it)
dpkg -l 'libbrotli*' | awk '/^ii/ {print $2, $3}' # Debian/Ubuntu
apk info -v 2>/dev/null | grep brotli # Alpine: brotli-libs
rpm -q brotli # RHEL family
# .NET and Bun
dotnet --list-runtimes # fixed: 8.0.33, 9.0.22, 10.0.14, 11.0.0-rc.3
bun --version # fixed: 1.4.4
# Node: process.version is the answer, process.versions.brotli is not
node -p "process.version + ' brotli ' + process.versions.brotli"Don’t verify with process.versions.brotli
I checked the Node.js source at each release tag. The fixed 22.23.4 still declares Brotli 1.1.0, and 24.22.1 and 26.12.1 still declare 1.2.0, because Node patched decode.c in place; process.versions.brotli reads that number, so it is identical before and after the fix. My own laptop answers v22.22.0 and brotli 1.1.0: vulnerable, and the brotli field would read the same after updating. Judge Node by process.version, and the system library by its package revision.
Step 2: Patch per environment
A fix on disk does nothing for a process that loaded the old code, so every card below ends in a restart, a rebuild or a redeploy.
Node.js on hosts and version managers
nodejs.org · 21 Oct
Install 22.23.4, 24.22.1 or 26.12.1, or later on each line, then restart every process: systemd units, pm2, Supervisor, cron jobs. HeroDevs points out that updating the npm brotli package changes nothing, because the bug lives in the runtime binary.
nvm install 24.22.1 && nvm alias default 24.22.1 volta install node@24.22.1 node -v # v24.22.1 or later sudo systemctl restart my-node-app # every service, worker and job
Docker images
Rebuild, don’t restart · pin tag + digest
The fixed official tags are on Docker Hub (node:22.23.4, node:24.22.1, node:26.12.1 and their -slim and -alpine variants), and the base tags were pushed again on 28 October. node:20.20.2 was last pushed on 22 April and will not change. Pin the tag and its digest, rebuild without cache, recreate the containers.
docker buildx imagetools inspect node:24.22.1-slim | grep -m1 Digest # FROM node:24.22.1-slim@sha256:<digest> docker compose build --pull --no-cache docker compose up -d --force-recreate docker compose exec app node -v
Distro packages
USN-8956-1 · DSA-6582-1 · DLA-4855-1 · RHSB-2026-015
Ubuntu’s nodejs links the system library, so Ubuntu marks it not affected and the fix is libbrotli (1.2.0-3ubuntu0.1 on 26.04, 1.1.0-2ubuntu0.1 on 24.04, 1.0.9-2ubuntu0.1 on 22.04). Debian fixed brotli in trixie (1.1.0-2+deb13u1) and bookworm (1.0.9-2+deb12u1), but still lists its nodejs as vulnerable there. Alpine’s nodejs and nodejs-current build with --shared-brotli (I read the APKBUILDs), so brotli-libs matters too. Red Hat’s bulletin was still ‘Ongoing’ on 27 October.
sudo apt-get update && sudo apt-get install --only-upgrade libbrotli1 apk upgrade brotli-libs nodejs # Alpine, as root # Processes that still map the replaced library: restart them sudo grep -l 'libbrotli.*(deleted)' /proc/[0-9]*/maps 2>/dev/null
Python
PyPI · 22 Oct · PYSEC-2026-4322 / 4323
Brotli 1.2.1 and brotlicffi 1.2.1.0 compile the fixed decoder. urllib3, requests, httpx and aiohttp aren’t vulnerable themselves; they decode br through these packages when they are installed. Regenerate lockfiles for Poetry, uv or pip-tools and rebuild images. If the bindings came from a distro package, the fix comes through the distro.
pip install -U "Brotli>=1.2.1" # only if pip show found it pip install -U "brotlicffi>=1.2.1.0" # same pip-audit # flags PYSEC-2026-4322 / 4323
.NET
Microsoft advisory · out-of-band, 25 Oct
Install runtime 8.0.33, 9.0.22, 10.0.14 or 11.0.0-rc.3 (SDKs 8.0.133/8.0.427, 9.0.123/9.0.320, 10.0.114/10.0.403) and restart apps. Self-contained apps must be rebuilt and redeployed; NativeAOT apps must be republished with the patched SDK, because the shared runtime doesn’t replace a decoder already linked into the executable. .NET Framework is not affected. If you already decompress Brotli and can’t update yet, Microsoft gives these changes, and warns against adding decompression you don’t already use:
// Only if you ALREADY call AddRequestDecompression: drop the br provider
builder.Services.AddRequestDecompression(options =>
{
options.DecompressionProviders.Remove("br");
});
// Only if you ALREADY set AutomaticDecompression: leave Brotli out
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};Bun, Envoy, Istio, Electron and the rest
Tenable · Electron releases · php.net
Bun: 1.4.4. Envoy and Istio: the fixed releases in the table above, or remove the Brotli decompressor filter in both directions until you upgrade. Electron 45.1.0, 44.10.0 and 43.8.4 ship Node.js 24.22.1, but each Electron app needs its vendor to ship a rebuild; I found no 42.x release with it. PHP on Windows: the -1 rebuilds. PHP’s brotli extension: 0.21.1, per Tenable.
bun upgrade && bun --version # 1.4.4 or later
Node.js 20, 23, 25 and older
No upstream fix · HeroDevs
Move to 24 or 26: 24 is in maintenance until April 2028, and 26 becomes LTS on 28 October, per the Node.js release schedule; 22 ends in April 2027. HeroDevs says it is preparing patched builds for its paid Never-Ending Support, starting with 20.x. Until you move, every mitigation below is permanent, not temporary.
Step 3: Mitigate what you can’t patch today
Node.js offers no workaround, and Rapid7 and Imperva both say only the upgrade is a fix. What you can do is keep untrusted Brotli away from the decoder, on the paths you actually use.
At the edge: refuse Brotli-encoded request bodies
Red Hat’s bulletin recommends stripping or rejecting Content-Encoding: br from untrusted clients at the proxy or load balancer. Rejecting is cleaner: a stripped header leaves the app with compressed bytes it can’t parse. $http_content_encoding is nginx’s standard variable for that request header.
# nginx, inside server {}: refuse Brotli-encoded request bodies
if ($http_content_encoding ~* br) {
return 415;
}
# Envoy / Istio: if you configured a Brotli decompressor, remove it
# in both directions until you run a fixed release (Tenable)In the app: Express inbound; fetch, axios and got outbound
inflate is a documented body-parser option (rejected bodies get a 415), and decompress is documented in both axios and got; the Brotli-only middleware is mine. Node’s built-in fetch() has no switch to turn decoding off: I read undici’s fetch code, and it only adds its default Accept-Encoding when you haven’t set one.
// Express 5 / body-parser 2: refuse every compressed body (same for raw, text, urlencoded)
app.use(express.json({ inflate: false }));
// ...or refuse only Brotli, before any parser, and keep gzip and deflate
app.use((req, res, next) => {
if (/\bbr\b/i.test(req.headers['content-encoding'] || '')) return res.sendStatus(415);
next();
});
// Outbound: stop asking for br (helps with honest servers only)
const response = await fetch(url, { headers: { 'accept-encoding': 'gzip, deflate' } });
// axios and got: no automatic decoding for URLs you do not control
await axios.get(url, { decompress: false });
await got(url, { decompress: false });What these mitigations cover, and what they don’t
- Edge rules protect inbound only: they do nothing for responses your services fetch from servers an attacker controls (Tenable, Aikido).
- Accept-Encoding is not a control: a hostile server can send br anyway. undici’s fetch, like urllib3, picks the decoder from the response’s Content-Encoding header, not from what you asked for.
- Python: uninstalling Brotli and brotlicffi removes the decoder from urllib3, requests and aiohttp; servers that insist on br may then fail (Aikido). Drop extras such as httpx[brotli] too.
- Envoy and Istio: disable the Brotli decompressor filter in both directions if you configured one (Tenable).
- Egress: Akamai suggests segmentation: restrict which hosts vulnerable services can reach, which narrows the fetch() path for user-supplied URLs.
Signals are weak, crashes are not
Imperva says unexpected process exits during decompression and decoder errors can help you prioritise, but neither is a reliable sign of exploitation on its own. Treat repeated worker crashes on endpoints that accept compressed bodies as worth a look, and follow ELLIO’s advice to restrict access to core dumps and how long you keep them. ELLIO published a source IP and a payload hash; a hit means isolating the host, not just blocking the address.
The order I’d work in
- 1. Today: internet-facing services that inflate request bodies (Express 5, body-parser 2, Koa) or fetch user-supplied URLs (webhooks, link previews, crawlers). Patch them, or put the edge and app mitigations in front now.
- 2. Also today: rebuild every image pinned to an older node tag and restart every long-running process.
- 3. By Friday 30 October: Python Brotli and brotlicffi, the system libbrotli, Bun, Envoy and Istio, and .NET apps that opted into Brotli decompression.
- 4. Before 10 November: a dated plan off Node.js 20, 23 and 25, and off .NET 8 and 9.
- 5. Every few days: re-check ClickHouse, Apache Arrow, FFmpeg, Debian’s nodejs, Red Hat’s errata and the Electron apps you run.
Conclusion
A week ago CVE-2026-111497 was the top line of a Node.js security release. Now it is a KEV entry with public exploits, and the real unit of exposure is the Brotli decoder, wherever it lives: Node.js, a Python wheel, a .NET app that opted in, an Envoy filter, an Electron app.
The good news is that exposure needs a path: untrusted Brotli has to reach a decoder. Patch the paths that face the internet today, rebuild and restart so the patch is real, put a mitigation in front of what you can’t patch, and use the 10 November deadline to retire Node.js 20 and .NET 8 and 9.
This week in one screen
Check
- • node -v everywhere, not process.versions.brotli
- • pip show Brotli brotlicffi
- • dotnet --list-runtimes, bun --version
Patch
- • Node 22.23.4 / 24.22.1 / 26.12.1
- • libbrotli, Brotli 1.2.1, .NET out-of-band
- • Rebuild images, restart processes
Watch
- • br bodies at the edge
- • fetch() to URLs you don’t control
- • EOL plan before 10 November
Sources
- Node.js — October 2026 security releases (CVE-2026-111497)
- CVE.org — CVE-2026-111497 record (Google as CNA, CVSS v4.0 9.2)
- Google Brotli — v1.2.1 release (also 1.1.1 and 1.0.10)
- CISA — Adds two known exploited vulnerabilities to catalog (27 Oct)
- Tenable — Brotli2Shell: frequently asked questions for CVE-2026-111497
- Imperva — Node2Shell: from Brotli data to remote code execution
- ELLIO — Node2Shell: RCE on stock Node.js builds
- Akamai — Inside Node2Shell: a closer look at CVE-2026-111497
- Rapid7 — ETR: CVE-2026-111497, out-of-bounds write in the Node.js Brotli decoder
- HeroDevs — CVE-2026-111497 on end-of-life Node.js
- Aikido — Python packages vulnerable to Node2Shell
- Microsoft — .NET security advisory for CVE-2026-111497
- Red Hat — RHSB-2026-015, Google Brotli decoder (node2shell)
- Ubuntu — CVE-2026-111497
- Debian security tracker — CVE-2026-111497
- body-parser — GHSA-27r3-jxmh-p828 (CVE-2026-111181)
- Help Net Security — Node2Shell / Broccoli2Shell (27 Oct)



