Read-only audit script for Node2Shell: CVE-2026-111497, an out-of-bounds write in the Brotli decoder that Node.js bundles in node:zlib. A process that decompresses attacker-chosen Brotli can crash, leak memory or run attacker code, with no login.
_ _ _ ___ ___ _ _ _
| \| |___ __| |___|_ )/ __| |_ ___| | |
| .` / _ \/ _` / -_)/ / \__ \ ' \/ -_) | |
|_|\_\___/\__,_\___/___||___/_||_\___|_|_|
CVE-2026-111497 Node.js Brotli decoder out-of-bounds write
When a Brotli stream signals a block switch that is invalid for its own structure, the vulnerable decoder reports "need more input" instead of a format error. The streaming decoder then keeps appending to an 8-byte internal buffer and writes past its end. The upstream fix returns a dedicated format error.
| Path | Who can reach it |
|---|---|
zlib.brotliDecompress*(), createBrotliDecompress() |
whoever controls the input you pass in |
fetch() (decodes Brotli responses by default) |
a hostile or compromised server you call: webhooks, user-supplied URLs, SSRF-ish features |
Express 5 / body-parser 2 (json, raw, text, urlencoded) |
anyone who can send a request: Content-Encoding: br bodies are decoded by default (inflate: true) |
A bare http.createServer() does not decode request bodies, so it is not reachable unless the application does it. Express 4 / body-parser 1 do not decode Brotli.
| Affected | Node.js 22.x before 22.23.4, 24.x before 24.22.1, 26.x before 26.12.1; all end-of-life lines (20.x, 23.x, 25.x, older) since they get no fix |
| Fixed upstream | 22.23.4, 24.22.1, 26.12.1 (2026-10-21). The advisory lists no workaround |
| Library | Google Brotli 1.0.3 through 1.2.0; fixed in 1.0.10, 1.1.1, 1.2.1 |
| Bun | fixed in 1.4.4 |
| Debian | brotli bookworm 1.0.9-2+deb12u1, trixie 1.1.0-2+deb13u1, sid 1.2.0-4.1. Debian's nodejs links the system library |
| Ubuntu | USN-8956-1/-2: brotli jammy 1.0.9-2ubuntu0.1, noble 1.1.0-2ubuntu0.1, resolute 1.2.0-3ubuntu0.1, plus ESM for 20.04 and older. nodejs is "not affected" on 22.04+ (system brotli); focal's nodejs vendors Brotli and is unfixed |
| RHEL family | Red Hat: Important, RHSB-2026-015. nodejs:22 uses system brotli, nodejs:24/26 bundle it; no fixed versions published yet |
| Alpine | brotli-libs 1.2.1-r0 (edge, 3.24, 3.23), 1.1.1-r0 (3.22, 3.21) |
| Status | CISA KEV reported 2026-10-27; ELLIO saw a first exploit attempt on 2026-10-26 against Node 24.22.0 x86-64 (it crashed the process); Imperva and Aikido reproduced code execution on the unmodified 24.22.0 image (not on every attempt). 24.22.1 rejects the input |
Distro states are a 2026-10-28 snapshot. Distros backport without bumping the version, and some Node builds link the system library, so node -v proves nothing on a distro build: the package changelog or the libbrotli package decides.
The same release fixes nine more CVEs (TLS checkServerIdentity hostname bypasses, dns.resolve NUL truncation, keep-alive reuse, randomUUID in snapshots, Permission Model). They are fixed by the Node.js update only: a libbrotli update does not touch them. The script reports them separately.
It only triages and changes nothing. It never sends a request, never runs a PoC and never edits configuration. It does not execute Node.js to learn its version.
- Finds every build: system and vendor prefixes (CloudLinux alt-nodejs, cPanel ea-nodejs, Plesk, snap,
/usr/local/n), version managers (nvm, fnm, volta, asdf, mise, n, nodenv,~/.bun),--node PATH, and every running node/bun process, including processes inside containers (via/proc/PID/exe). - Reads the version from the binary: Node compiles
nodejs.org/download/release/vX.Y.Z/node-vX.Y.Z.tar.gzinto itself. Only a binary without that string is run with--version, and only when no other user could have replaced it. - Tells bundled from linked: a binary that names
libbrotlideclinks the system library, so the libbrotli package decides (changelog, then distro matrix, then upstream thresholds). - Decides per build:
- the owning package's changelog naming CVE-2026-111497 always wins
- linked builds: the libbrotli package, as above
- bundled builds: the upstream thresholds; a distro-owned build below them with no readable changelog is UNKNOWN, a NodeSource build is judged by version
- vendor builds (
/opt/alt,/opt/cpanel,/opt/plesk) with invisible package data are UNKNOWN (backports, CageFS)
- Works out what is in use: running processes, whether they listen on a non-loopback or loopback address (
ss), and whether a process still runs a binary or a libbrotli that the update replaced ((deleted)in/proc/PID/exeor/proc/PID/maps): patched on disk, not restarted. - Looks for exposure in applications: Express ≥ 5 and body-parser ≥ 2 under the app roots and the working directories of running processes, and direct
createBrotliDecompress/brotliDecompresscalls outsidenode_modules. - Lists related copies (not part of the verdict): the system libbrotli and Python
Brotli/brotlicffipackages below 1.2.1 / 1.2.1.0. - Hunts: crash lines (
node[pid]: segfault,BROTLI_DECODER_ERROR) in the journal and pm2 logs, shells/downloaders spawned by a listening node/bun server, a runtime started from/tmp,/dev/shmor a memfd, node containers that exited 139/134, and the one network indicator published so far. - Produces a risk score from 0 to 100 and a verdict.
It runs without root; it then sees only what that user can read (other users' processes and containers need root).
- PATCHED: every affected build carries the fix.
- VULNERABLE: at least one unfixed build is running or installed system-wide (
/usr,/opt,/snap, …). Any script on the host may call a system-widenode, andfetch()is a client-side path. - MITIGATED: unfixed builds exist, but only as dormant version-manager leftovers (nvm and friends) that nothing runs.
nvm useor a service pointed at one reopens it at once. A build named with--nodecounts as in use. - UNKNOWN: a build in use could not be classified.
- NOT_APPLICABLE: no Node.js 10.16+ (the first with Brotli in
node:zlib) and no Bun.
Exposure (listeners, Express 5/body-parser 2) never changes the verdict; it only raises the score. A build that is patched on disk but still running the old binary or library is reported VULNERABLE: restart it.
oct2026_other_cves is FIXED, AFFECTED, CHECK or NOT_APPLICABLE, taken over the builds in use. It never changes the verdict, the exit code or the score. Debian's nodejs is the typical case: the libbrotli update makes it PATCHED for Node2Shell while it is still AFFECTED by the TLS and dns CVEs. In --quiet mode a second line Oct-2026-other-CVEs <status>: <reason> follows the verdict line.
./node2shell-audit.sh # full text report
./node2shell-audit.sh --json # machine-readable JSON, no banner
./node2shell-audit.sh --quiet --no-hunt # one-line verdict for mass scans
./node2shell-audit.sh --node /opt/app/bin/node # audit an extra build (repeatable, counts as in use)
./node2shell-audit.sh --app-root /srv/apps # scan this tree for Express 5 / body-parser 2 (repeatable)
./node2shell-audit.sh --no-apps # skip the application and Python scans
./node2shell-audit.sh --since 2026-10-21 --until 2026-10-28 # IoC window
./node2shell-audit.sh --versionWithout --app-root it scans /srv, /var/www, /opt, global node_modules, /root, /home (or $HOME without root) and the working directory of every running node process, down to depth 9, each with a two-minute limit. On a large /home use --app-root or --no-apps.
On a host without a copy of the script:
ssh user@host 'bash -s -- --no-banner' < node2shell-audit.sh| Code | Meaning |
|---|---|
| 0 | PATCHED or NOT_APPLICABLE |
| 1 | MITIGATED (unfixed builds, none running or system-wide) |
| 2 | VULNERABLE (an unfixed build is running or system-wide) |
| 3 | UNKNOWN (treat as suspect on anything public-facing) |
| 4 | ERROR (script could not run; missing tools or bad environment) |
Same as php2shell-audit, cve-2026-99999-audit, cve-2026-46331-audit and linux-pagecache-cve-audit.
See examples/sample-output.txt for a Debian 12 host with distro nodejs 18 linked to an unpatched libbrotli, an Express 5 API on 0.0.0.0:3000, a container running Node 24.22.0 and a leftover nvm 22.23.3, and examples/sample-output.json for the JSON form. Regenerate them with bash tests/make_examples.sh.
| Signal | Weight |
|---|---|
Verdict VULNERABLE or MITIGATED |
+50 |
Verdict UNKNOWN |
+30 |
| An unfixed or unknown build is in use | +10 |
| …and it listens on a non-loopback address | +15 |
| …on an end-of-life line | +5 |
| Express 5 / body-parser 2 found, or direct Brotli calls | +10 |
| A process still runs a replaced binary or library | +5 |
IoCs (crashes, spawned shells, runtime in /tmp, container crashes, the ELLIO IP) |
+10 |
Capped at 100. A dormant nvm leftover scores 50, an unfixed system node 60, one listening publicly 75.
ansible -i inventory all -m script -a "node2shell-audit.sh --json --no-hunt --no-apps" --become | tee /tmp/audit.jsonl- name: audit Node2Shell
ansible.builtin.script: node2shell-audit.sh --quiet --no-hunt
register: audit
failed_when: audit.rc == 2
changed_when: falseUpdate, then restart every node process (systemctl restart …, pm2 reload all): a running process keeps the old binary and the old libbrotli. Rebuild container images on a patched base; a tag like node:24 can be old, and node:22.23.3-alpine stays vulnerable until rebuilt. Node.js 20 and 25 will not be fixed: move to 22, 24 or 26.
There is no workaround in the Node.js advisory. To shrink the exposure until you can patch:
- Express / body-parser:
express.json({ inflate: false })(and the same forraw,text,urlencoded). Compressed request bodies then get HTTP 415. - Reverse proxy / WAF: reject request bodies with
Content-Encoding: br. - Outbound: do not
fetch()URLs you do not control; restrict egress from vulnerable hosts. - Network: keep Node services off the public network.
- Single-executable apps (
pkg,nexe, Node SEA) are only found when the process is namednode/bun. Electron apps carry Chromium's Brotli, not Node's. Deno uses a Rust implementation; I found no statement either way and do not scan it. - Container images that are not running are invisible; so is anything inside a container's own package manager. A container build that links the system libbrotli is UNKNOWN.
- Other software with its own Brotli copy is listed in the vendors' advisories, not scanned: curl/libcurl (
--compressed), nginx/Envoy Brotli filters (Envoy fixed in 1.40.0 / 1.39.3 / 1.38.6 / 1.37.8, only when the Brotli decompressor filter is configured), Istio, .NET (8.0.33 / 9.0.22 / 10.0.14), Java Netty with brotli4j (1.24.0), Python packages outside the paths scanned. Apachemod_brotlionly compresses and is not affected. - A static scan cannot see
inflate: falseset in application code, or request bodies decoded by a framework other than Express. - The app scan reads
node_modules/*/package.jsonversions; a bundled or vendored copy of body-parser is not seen. - The hunt greps whole log files, not by date (only the journal honours
--since/--until). A clean exploit leaves no crash, so zero IoCs proves nothing. The ELLIO IP (94.154.43.7) and body hash come from a single vendor and are unconfirmed; Node.js, Google, CISA, Imperva and Vercel published no IoCs. - As root the script reads other users' homes but never executes anything in them.
bash tests/run_tests.sh # 91 cases: fake builds + fake /proc + mocked dpkg/rpm/apk/ss/ps/journalctl/docker; must not run as rootThe test hooks (NODE2SHELL_TEST_PATH, NODE2SHELL_TEST_ROOT) are ignored when EUID is 0. Every JSON case also asserts the mocked hostname, which catches mocks silently bypassed by the pinned PATH. Version detection was also checked against the official node-v22.23.3 and node-v24.22.0 linux-x64 binaries (SHA-256 from SHASUMS256.txt) running as live processes, including a binary replaced while running.
- Node.js: https://nodejs.org/en/blog/vulnerability/october-2026-security-releases (22.23.4 / 24.22.1 / 26.12.1)
- Akamai: https://www.akamai.com/blog/security-research/inside-node2shell-closer-look-cve-2026-111497
- Aikido: https://www.aikido.dev/blog/node2shell-nodejs-brotli-rce-cve-2026-111497 and https://www.aikido.dev/blog/python-packages-vulnerable-node2shell-brotli-cve-2026-111497
- Brandefense: https://brandefense.io/blog/node2shell-nodejs-brotli-rce-cve-2026-111497/
- Rescana (KEV, ELLIO indicators): https://www.rescana.com/post/nodejs-cve-2026-111497-node2shell-brotli-rce-october-2026
- Debian: https://security-tracker.debian.org/tracker/CVE-2026-111497
- Ubuntu: https://ubuntu.com/security/CVE-2026-111497
- Red Hat: https://access.redhat.com/security/cve/cve-2026-111497