Operating system, libraries and runtimes
Node.js: Brotli decoder guard (CVE-2026-111497)
Node.js October 2026 Mitigation
Node.js 22.23.4, 24.22.1 and 26.12.1 (21 October 2026) fix ten CVEs. The serious one is CVE-2026-111497, an out-of-bounds write in the Brotli decoder that can lead to remote code execution. Any code that decodes attacker-controlled Brotli reaches it, and on an unfixed runtime that includes every fetch(), because fetch decodes Content-Encoding: br responses on its own. Node lines 25 and older are end of life: move to 22.23.4, 24.22.1 or 26.12.1.
CVE-2026-111497 is also known as Broccoli2Shell (it was first called Node2Shell). It is rated High by Node.js and 9.2 Critical by GitHub. Three kinds of code reach it on an unfixed runtime:
- Anything that fetches a URL. Built-in
fetch()decodes the response, so any server you talk to can send the payload. - Anything that accepts request bodies. Express 5.0.0 to 5.3.0 and body-parser 2.0.0 to 2.3.0 inflate Brotli request bodies in parsers such as
raw()andjson(). Express and body-parser do not contain the bug, so upgrading them does not help. A barenode:httpserver does not decode bodies by itself. - Anything that calls the decoder directly:
zlib.brotliDecompress*()andcreateBrotliDecompress(), including libraries that use them on archives, caches or queue messages.
Node.js bundles its own Brotli, so the system libbrotli does not matter for Node. Only the Node.js version does. You can read the bundled version with node -p "process.versions.brotli". The Imperva Red Team has run attacker-supplied code on the official, unmodified Node.js 24.22.0 arm64 GNU/Linux image, through both a fetched response and an Express request body, so treat this as exploitable. Node.js has published no workaround of its own: upgrading the runtime is the only complete fix, and the guard below is our protective measure for services you cannot upgrade yet. After upgrading, restart every process and rebuild every container image, because a running process keeps the old decoder until it restarts.
We wrote these for the Node services we couldn't upgrade the same day. There are two parts:
brotli-guard.cjsis a small file Node loads before your application, through--require. It makes Node's Brotli decoder refuse to run, for CommonJS and ES modules alike, and stopsfetch()asking servers forbr. Compression keeps working, and so do gzip and deflate. A blocked decode fails the way corrupt input does, so code that already copes with a bad compressed body copes with this too. On a fixed runtime it goes idle by itself.node-oct2026-mitigate.shfinds every Node runtime and process on the host, tells you which are exposed, finds the code that decodes Brotli, and loads the guard into systemd services for you. It checks the guard works on each service's own runtime before installing it, and rolls back if the service won't start with it.
Node.js quick start
-
Download both files into the same directory. The script looks for
brotli-guard.cjsnext to itself. The repository is mirrored on GitHub and Codeberg; pick a base URL.# BASE from GitHub: https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main # BASE from Codeberg: https://codeberg.org/PrivacySafe/security-patches/raw/branch/main # curl -fLO "$BASE/node/node-oct2026-mitigate.sh" -fLO "$BASE/node/brotli-guard.cjs" curl -fLO https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main/node/node-oct2026-mitigate.sh \ -fLO https://raw.githubusercontent.com/PrivacySafe/security-patches/refs/heads/main/node/brotli-guard.cjs -
Read them before running them. Always.
less node-oct2026-mitigate.sh brotli-guard.cjs -
Make the script executable.
chmod +x node-oct2026-mitigate.sh -
See what's exposed. This changes nothing. Root is optional, but without it you won't see other users' runtimes and processes.
sudo ./node-oct2026-mitigate.sh check -
Find what in your applications decodes Brotli, and any single executable applications or startup snapshots you'll need to rebuild. Also read-only.
sudo ./node-oct2026-mitigate.sh scan -
Test the guard on your runtime. Every check should pass.
node brotli-guard.cjs --self-test -
Guard each systemd service you can't upgrade yet.
sudo ./node-oct2026-mitigate.sh guard --unit myapp
To confirm GitHub and Codeberg serve the same files, compare checksums:
sha256sum node-oct2026-mitigate.sh brotli-guard.cjs
curl -fsSL https://codeberg.org/PrivacySafe/security-patches/raw/branch/main/node/node-oct2026-mitigate.sh | sha256sum
curl -fsSL https://codeberg.org/PrivacySafe/security-patches/raw/branch/main/node/brotli-guard.cjs | sha256sum
After guarding, let real traffic run and check what the guard blocked. Each blocked call names the code that tried to decode Brotli:
sudo ./node-oct2026-mitigate.sh status
Once Node is upgraded, take the guard out again:
sudo ./node-oct2026-mitigate.sh unguard --unit myapp
Node.js commands
| Command | What it does |
|---|---|
check (default) |
Reports every Node runtime on disk, every running Node process and every container image that declares NODE_VERSION, with whether each is fixed. Flags processes still running a binary that's been replaced on disk, processes that are guarded, and processes using startup snapshots. Changes nothing, root optional. Exits 0 if nothing running is exposed, 1 if unfixed Node is running without the guard, 2 if some version couldn't be read |
scan |
Read-only. Finds application code and dependencies that call Node's Brotli decoder, grouped by application, plus single executable applications and startup snapshots that must be rebuilt (CVE-2026-106074). Exits 0 if it found nothing, 1 if it found something |
guard --unit U |
Installs the guard to /usr/local/lib/privacysafe/brotli-guard.cjs, self-tests it on U's runtime as U's user, and adds it to NODE_OPTIONS through a systemd drop-in. Then restarts U, checks it stays up and the guard is live, and rolls back if not. Skips a runtime that's already fixed (--force guards anyway), and doesn't start a service you'd stopped. Repeat --unit to guard several services |
unguard --unit U |
Removes the drop-in, restarts U and checks it comes back without the guard. Refuses to touch a drop-in this script didn't write |
status |
Lists guarded services, their Node version and whether it's fixed, and the call sites the guard blocked in the last 7 days. Tells you when a service is fixed and can be unguarded |
hunt |
Read-only log triage. Scans the journal, syslog, kernel log and pm2 logs for Node segfaults, V8 fatal errors, Brotli decoder frames in crashes, services killed by a crash signal, and guard events. Exits 0 if nothing matched, 1 if it found something to look at |
version |
Prints the script version. Every other command prints it on its first line too |
What check looks at
- Runtimes on disk. Everything on
PATH, the usual system locations (/usr/bin,/usr/local/bin,/opt, snap), and every user's version manager: nvm, n, fnm, volta, asdf and mise. - Running processes. Every Node process, including renamed copies of the binary, with its user and systemd unit. It flags a process whose binary was replaced on disk since it started, because it still runs the old code until you restart it.
- Container images. Images in Docker or Podman whose metadata declares
NODE_VERSION, which covers the officialnodeimages and most images built on them. It reads image metadata only. Nothing is started. Images that install Node another way aren't detected. - Places it can't see. Electron apps bundle their own Node and need an update from their vendor. The fixed Node releases also update Node's built-in CA store for the eMudhra compromise; see below.
The script reads each Node version from the binary's contents instead of running it, so root never executes a binary planted in a user's ~/.nvm or inside a container. It only runs a binary when it can't read the version, and only if root owns it and nobody else can write to it.
Node.js patch detection
For each runtime, three checks run in order, and the first that applies decides:
FIXED_BUILDS. Distro builds you list as fixed, for a backport the script doesn't know about. A build only counts for the same upstream version.- Changelog. For a runtime installed from a Debian or Ubuntu package, the package changelog names CVE-2026-111497. The changelog only counts if its newest entry is the installed version. This catches a distro that backports the fix to an end-of-life line without changing the upstream version, for example to the Node 20 a stable release ships.
- Upstream version. The runtime is at or above the first fixed release for its line: 22.23.4, 24.22.1 or 26.12.1. Lines 25 and older read as not fixed unless a backport is present.
The result is fixed, not fixed, or unknown. Unknown means the version couldn't be read, usually because the script wasn't run as root.
The guard on its own
guard needs systemd. Anywhere else, load the guard yourself. It has to load before your application code, so use --require or NODE_OPTIONS:
# directly
node --require /usr/local/lib/privacysafe/brotli-guard.cjs app.js
# through the environment, for anything that starts node for you
NODE_OPTIONS="--require=/usr/local/lib/privacysafe/brotli-guard.cjs" npm start
# pm2
pm2 start app.js --node-args="--require=/usr/local/lib/privacysafe/brotli-guard.cjs"
In a container, copy the file into the image and set ENV NODE_OPTIONS=--require=/path/to/brotli-guard.cjs. When the guard loads, it prints one line to stderr, [brotli-guard] active ..., so you can confirm it from the service's log.
Its behavior is set through the environment:
| Variable | Default | Purpose |
|---|---|---|
BROTLI_GUARD |
block |
block refuses to decode. log decodes normally but reports each caller, so you can see what uses Brotli before you block it. off does nothing |
BROTLI_GUARD_FORCE |
(unset) | 1 keeps the guard active on a runtime that already has the fix |
BROTLI_GUARD_QUIET |
(unset) | 1 drops the startup line. Blocked calls are still reported |
Node.js hunting for exploitation
A crash in Node's Brotli decoder doesn't show in application logs. It shows as a dead process. hunt reads the journal and system logs, never changes anything, and reports what it finds:
sudo ./node-oct2026-mitigate.sh hunt
It looks for Node segfaults and protection faults in the kernel log, V8 fatal errors, Brotli decoder frames in crash backtraces, services killed by SIGSEGV, SIGABRT or SIGBUS, and every decode the guard blocked. HUNT_SINCE sets how far back it reads.
A match tells you exactly where to look. Buggy native addons crash Node too, and a segfaulting service may not be running Node at all. A crash in the Brotli decoder on an unfixed runtime is worth treating as an incident: check what the process could reach (secrets in its environment, database credentials, API tokens) and rotate them. A blocked decode on a route attackers can reach tells you which request to look at.
Node.js configuration
| Variable | Default | Purpose |
|---|---|---|
NODE_FIXED_22, NODE_FIXED_24, NODE_FIXED_26 |
22.23.4, 24.22.1, 26.12.1 |
First fixed release for each line |
FIXED_BUILDS |
(empty) | Extra fixed distro builds, space-separated full versions, for example a Debian backport to Node 20 |
SCAN_PATHS |
/srv /opt /var/www /home /usr/local/lib /usr/lib/node_modules |
Where scan looks for code, SEAs and snapshots |
SCAN_DEPTH |
8 |
How deep scan goes |
GUARD_DST |
/usr/local/lib/privacysafe/brotli-guard.cjs |
Where guard installs the guard |
HUNT_SINCE |
2026-10-14 |
How far back hunt reads the journal |
Before you run it
- This is not a patch. Upgrade to Node.js 22.23.4, 24.22.1 or 26.12.1, restart, then run
unguard. Node 25 and older will never be fixed; move to a supported line. - The guard only covers CVE-2026-111497. The other nine CVEs, CVE-2026-105481, CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106070, CVE-2026-106071, CVE-2026-106072, CVE-2026-106073 and CVE-2026-106074 (TLS hostname checks, DNS, keep-alive sockets, the permission model, ZipFile, random value caching), have no workaround here.
- The guard covers Node's decoder as reached from JavaScript. A native addon that calls the bundled Brotli library directly isn't covered, and neither is code that saved a reference to the zlib functions before the guard loaded. That's why it must load first.
- Anything that really needs Brotli will fail. A service that fetches from a server sending Brotli whatever it's asked for, or accepts Brotli-encoded request bodies, gets a decode error until you upgrade. Run with
BROTLI_GUARD=logfirst if you're not sure. guardrefuses when anEnvironmentFilesetsNODE_OPTIONS, because that would silently override the drop-in. Add--require=/usr/local/lib/privacysafe/brotli-guard.cjsthere yourself.- Processes that reset
NODE_OPTIONSwon't carry the guard. Some wrapper scripts, pm2 ecosystem files and containers do this.guardwarns when it can't confirm the guard is live. - Single executable applications and startup snapshots must be rebuilt with a fixed Node (CVE-2026-106074). Treat
crypto.randomUUID()andrandomInt()values they produced as guessable, and rotate any that need to be secret. - Containers get their own fix. Each image carries its own Node. Rebuild on a fixed base image.
checklists the images it can see. - GNU/Linux only.
check,scanandhuntrun on any GNU/Linux;guardandunguardneed systemd. - Drop-ins go to
/etc/systemd/system/<unit>.d/50-privacysafe-brotli-guard.conf, and each records the script version that wrote it. - A pre-release is not the release.
v24.22.1-pre,-rc.0and-nightly…are builds made before 24.22.1, so at a fix version they are reported as unknown and the check exits2. One past the fix version is fixed. A runtime that cannot be read (including one the script declines to execute because of who owns it) is reported as unknown and no longer ends the run. - Runtimes are found anywhere on
PATH, as well as in the usual install locations and the common version-manager directories.
Red Hat advisories
Red Hat's advisory list (security advisories released 2026-04-01 to 2026-11-03) names the CVEs below. A Red Hat build is judged by its RPM release, not by the upstream version number, because Red Hat backports fixes: the advisory names the release that fixes it. A CVE with no advisory in the list has none for that period, which is not the same as not affected. The current state is on Red Hat's CVE database.
| Advisory | Released | Severity | Package and stream | CVEs on this page |
|---|---|---|---|---|
| RHSA-2026:87702 | 2026-10-22 | Important | nodejs22-22.23.4-2.3.2.3.hum1 (Hummingbird) |
CVE-2026-105482, CVE-2026-105483 |
| RHSA-2026:87703 | 2026-10-22 | Important | nodejs26-26.12.1-0.1.hum1 (Hummingbird) |
CVE-2026-105482, CVE-2026-105483 |
| RHSA-2026:89206 | 2026-10-25 | Important | nodejs24-24.22.1-3.hum1 (Hummingbird) |
CVE-2026-105483 |
| RHSA-2026:92376 | 2026-10-29 | Important | nodejs-24.22.1-1.module+el8.10.0+25218+d9e131cb (RHEL 8.10); nodejs-nodemon-3.1.14-2.module+el8.10.0+24711+b610b202 (RHEL 8.10); nodejs-packaging-2021.06-6.module+el8.10.0+24711+b610b202 (RHEL 8.10) |
CVE-2026-105481, CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106071, CVE-2026-106072, CVE-2026-106073, CVE-2026-106074 |
| RHSA-2026:92378 | 2026-10-29 | Important | nodejs-24.22.1-3.module+el9.8.0+25216+afa2a4a4 (RHEL 9.8); nodejs-nodemon-3.1.14-3.module+el9.8.0+24708+e71d2e1d (RHEL 9.8); nodejs-packaging-2021.06-6.module+el9.8.0+24708+e71d2e1d (RHEL 9.8) |
CVE-2026-105481, CVE-2026-105482, CVE-2026-105483, CVE-2026-106069, CVE-2026-106071, CVE-2026-106072, CVE-2026-106073, CVE-2026-106074 |
CVE-2026-111497: 71 advisories, listed in the Red Hat advisories section of the Brotli README.
The Security Patches software and this documentation are dedicated to the public domain under CC0 1.0 Universal. Other pages on privacysafe.app/docs are CC BY-SA 4.0 unless the page says otherwise.