SecurityNode.js

Node2Shell (CVE-2026-111497): Exploited, in CISA KEV, and Bigger Than Node.js

October 28, 2026
13 min read
A hydraulic press compressing a clear glass cube that cracks with glowing orange fractures in a dim workshop
Share:

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)

27 FebGoogle commits the fix to google/brotli as 0ed4268a, ‘Detect and report invalid stream earlier.’ No release, no advisory.
Early OctEnvoy and Istio ship fixes without naming the flaw (Tenable).
21 OctNode.js 22.23.4, 24.22.1 and 26.12.1. Google tags Brotli 1.0.10, 1.1.1 and 1.2.1, each listing CVE-2026-111497 under SECURITY.
22 OctGoogle publishes the CVE record. Brotli 1.2.1 and brotlicffi 1.2.1.0 reach PyPI, Bun ships 1.4.4, and the first public proof of concept appears on GitHub (HeroDevs). Imperva shows code execution on the official Node.js 24.22.0 linux arm64 image.
25 OctMicrosoft ships out-of-band .NET 8.0.33, 9.0.22, 10.0.14 and 11 RC3.
26 OctELLIO records exploit traffic at 15:08 UTC. Ubuntu publishes USN-8956-1 for libbrotli, Red Hat opens RHSB-2026-015, and PHP rebuilds its Windows packages with Brotli 1.2.1.
27 OctCISA adds the CVE to KEV. Electron 45.1.0, 44.10.0 and 43.8.4 move to Node.js 24.22.1.

How each source rates it

SourceRatingWhat to read into it
Google (CNA, CVE record)CVSS v4.0 9.2, CriticalAT:P: the attack needs a precondition, a path that decodes untrusted Brotli
Node.jsHighNo CVSS in the advisory, and no workaround offered
Microsoft (.NET)CVSS v3.1 8.1AC:H; .NET is exposed only when an app opts into Brotli decompression
Red HatImportant, CVSS v3.1 8.1RHSB-2026-015 lists RHEL 7 to 10; errata still in progress on 27 October
UbuntuMedium priorityIts nodejs packages link the system libbrotli and are marked not affected
CISA KEVAdded 27 Oct, due 10 NovListed 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

ProductFixed inExposed when
Google Brotli (C library)1.0.10 · 1.1.1 · 1.2.1Anything decoding untrusted br with 1.0.3 to 1.2.0
Node.js22.23.4 · 24.22.1 · 26.12.1Untrusted br reaches node:zlib: body parsers, fetch(), your own calls
Python Brotli / brotlicffi1.2.1 / 1.2.1.0Installed next to urllib3/requests, httpx[brotli] or aiohttp
.NET 8 / 9 / 10 / 118.0.33 · 9.0.22 · 10.0.14 · 11 RC3Only when an app opts into Brotli request or response decompression, or calls BrotliStream or BrotliDecoder
Bun1.4.4Versions 1.0.21 to 1.4.3, per Tenable
Envoy / IstioEnvoy 1.36.12, 1.37.8, 1.38.6, 1.39.3, 1.40.0 · Istio 1.29.9, 1.30.6, 1.31.2Only with a Brotli decompressor configured
PHP on Windows8.2.35-1 · 8.3.36-1 · 8.4.27-1 · 8.5.12-1cURL’s automatic decompression enabled (off by default)
FFmpeg · ClickHouse · Apache ArrowNo fixed release listed on 26 OctJFrog, 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

PathWhen it decodesIf you can’t patch today
Express 5 / body-parser 2 (json, raw, text, urlencoded)By default: inflate is trueinflate: false, or reject br before the parsers or at the proxy
Koa with @koa/bodyparserListed by ELLIO among its working targetsReject br at the proxy
Bare node:http serverNever on its ownSearch your handlers for zlib Brotli calls
Built-in fetch()Whatever Content-Encoding the response carriesPatch; restrict egress for user-supplied URLs
axios, gotdecompress defaults to truedecompress: false for URLs you don’t control
Your own node:zlib callszlib.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. 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. 2. Also today: rebuild every image pinned to an older node tag and restart every long-running process.
  3. 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. 4. Before 10 November: a dated plan off Node.js 20, 23 and 25, and off .NET 8 and 9.
  5. 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

Diego Rodriguez

Diego Rodriguez

Senior Full-Stack & AI Engineer

Diego has 10+ years of experience building production-grade AI-powered applications, from LLM orchestration and RAG pipelines to ML-driven risk detection and algorithmic trading systems.

Learn more about Diego →