Like a mail room that auto-opens every package — including the ones rigged to explode
CVE-2026-111497 is an out-of-bounds write in Google's Brotli decompression library as bundled in Node.js's zlib module. The decoder's fast-path discards the error return from DecodeBlockTypeAndLength() when it encounters a block-type switch in a stream that declares only a single block type. A crafted Brotli payload can trigger a heap write past the allocated buffer. The flaw affects every supported Node.js release line: 22.x (before 22.23.4), 24.x (before 24.22.1), and 26.x (before 26.12.1). Critically, the vulnerable code path fires automatically — Node.js's built-in fetch() calls zlib.createBrotliDecompress() whenever a response carries Content-Encoding: br, regardless of what the caller requested in Accept-Encoding. A malicious HTTP server only needs to reply with that header and the trigger payload.
Node.js tagged this HIGH, which aligns with reality. The advisory describes RCE potential, and the automatic decompression surface through fetch() widens exposure well beyond applications that explicitly call zlib.brotliDecompress(). However, there is meaningful friction: a researcher's fuzzer ran 1,200+ structured mutations without producing a single crash, and no weaponized exploit exists publicly. The OOB write is real — confirmed by the upstream Google Brotli cherry-pick commit 0ed4268a4401 — but converting it to reliable code execution requires deep understanding of the Node.js heap layout. noisgate assesses this at HIGH (8.1): the automatic decompression surface is too broad to dismiss, but the exploitation difficulty prevents escalation to CRITICAL.
4 steps from start to impact.
Attacker controls an HTTP endpoint
- Target Node.js application makes outbound HTTP requests (fetch(), http.get(), axios, etc.)
- Target requests a URL the attacker can influence — webhook callback, user-supplied URL for metadata/preview, SSRF, or a compromised upstream API
- Many Node.js services only talk to trusted internal APIs and never fetch attacker-supplied URLs
- Corporate egress proxies and DNS sinkholing may prevent the Node.js process from reaching attacker infrastructure
Malicious server returns Content-Encoding: br with crafted payload
Content-Encoding: br and a Brotli-compressed payload containing a malformed metablock: a block-type switch command in a stream declaring only one block type. Node.js's fetch() auto-decompresses without checking Accept-Encoding, so the client cannot opt out. The decompression is synchronous on the libuv thread pool.- Response must reach the Node.js process's HTTP client stack unmodified — no intermediate proxy that strips or re-encodes Content-Encoding
- CDNs and reverse proxies (Cloudflare, nginx, AWS ALB) often re-encode or strip Content-Encoding on egress responses, which would neutralize the payload
- Content-Encoding: br in a response from an IP that doesn't normally serve Brotli is anomalous and detectable
Brotli decoder triggers OOB write
BrotliDecoderDecompressStream(), the fast-path code calls DecodeBlockTypeAndLength() and discards the error return. When the stream header declares num_block_types == 1 but the bitstream contains a block-switch command, the decoder writes past the end of the allocated ring buffer. The write offset and value are partially attacker-controlled via the crafted bitstream. The upstream fix (commit 0ed4268a4401) adds a validation check that immediately returns BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH.- Node.js version is unpatched (< 22.23.4, < 24.22.1, or < 26.12.1)
- The crafted payload must survive any intermediate decompression/recompression
- After 1,200+ structured fuzzing iterations, the CaterMaoZi research toolkit produced zero crashes — reaching the OOB write with exploitable parameters is non-trivial
- Heap ASLR and guard pages in modern allocators (glibc, jemalloc) reduce the reliability of heap corruption exploitation
BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH error constant in patched versions provides a precise detection signalHeap corruption escalated to code execution
- Attacker has detailed knowledge of the target's Node.js version, OS, and memory layout
- The OOB write offset and value must be precisely controlled — not just 'some bytes past the buffer'
- V8's garbage collector and compacting heap make heap layout prediction extremely difficult
- No public exploit chain exists; the CaterMaoZi toolkit explicitly states 'this CVE has no publicly disclosed exploitation code'
- ASLR, NX/DEP, and stack canaries all add exploitation barriers
{ compress: false } to fetch() calls or use Accept-Encoding: identity headers. While a malicious server can still send Content-Encoding: br, Node.js will not auto-decompress if compression is disabled at the client level. This is a code change and may require application-level testing.mprotect(PROT_EXEC), or shellcode patterns — can catch the post-exploitation phase even if the OOB write succeeds.- WAF on inbound traffic — This vulnerability triggers on *outbound* HTTP responses received by the Node.js client, not on inbound requests. Your WAF protects the front door; the exploit comes through the back door when your server calls out.
- Sending
Accept-Encoding: identity— The attacker's server controls the response headers. Even if the client doesn't request Brotli, a malicious server can respond withContent-Encoding: brand Node.js will decompress it. Only disabling compression handling in the fetch client itself works. - Upgrading only the system Brotli library — Node.js bundles its own copy of Brotli in
deps/brotli. System-level Brotli package updates (e.g.,libbrotli1on Debian) do not affect Node.js's bundled copy. You must update Node.js itself.
The supporting signals.
| In-the-wild exploitation | None reported. Not in CISA KEV. Node.js advisory states no known exploitation. Disclosed 2026-10-21 — only 1 day old at time of assessment. |
|---|---|
| Proof-of-concept | Research toolkit only. CaterMaoZi/CVE-2026-111497-POC on GitHub (3 stars) provides probe, fuzz, send, and compression-bomb modes. Explicitly states: *'This CVE has no publicly disclosed exploitation code.'* After 1,200+ fuzzing iterations, zero crashes produced. Listed on pocindex.io. |
| EPSS | Not yet scored. CVE is < 48 hours old; FIRST EPSS model has not ingested it. Expect initial score within 7–14 days. Historically, Node.js OOB-write CVEs with no public exploit land in the 5th–15th percentile range. |
| CISA KEV | Not listed. No federal mandate to patch by a specific date. |
| CVSS vector (noisgate estimate) | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H → 8.1 HIGH. AC:H reflects the requirement that the target must fetch attacker-controlled content AND the attacker must craft a payload that survives the narrow trigger window. No vendor-published CVSS vector exists. |
| Affected versions | Node.js 22.0.0 – 22.23.3, 24.0.0 – 24.22.0, 26.0.0 – 26.12.0. The bug is in the bundled Google Brotli library (deps/brotli), not Node.js-original code. Any software embedding the same Brotli commit range may be affected independently. |
| Fixed versions | 22.23.4 (LTS Krypton), 24.22.1 (LTS), 26.12.1 (Current). Fix authored by Evgenii Kliuchnikov (Google, upstream Brotli) via cherry-pick 0ed4268a4401abcb44720963; Node.js integration by Antoine du Hamel (@aduh95). Distro backports: check nodejs packages in Debian/Ubuntu/RHEL trackers — expect 2–5 day lag. |
| Scanning/exposure | Shodan indexes millions of Node.js-powered HTTP servers. However, this is a client-side decompression vulnerability — the target is the Node.js process *making* the outbound request, not a listening server. Traditional internet-facing exposure metrics are less relevant; internal asset inventory of Node.js runtimes is the key metric. |
| Reporter | Reported by Matteo Collina (mcollina), Node.js Technical Steering Committee member. Upstream fix by Evgenii Kliuchnikov (Google Brotli maintainer). |
| Disclosure date | 2026-10-21 (coordinated release across all three Node.js branches simultaneously). |
Sources.
- Node.js October 2026 Security Advisory
- Node.js v26.12.1 Release Notes
- Node.js v24.22.1 Release Notes
- CaterMaoZi PoC Toolkit (pocindex.io listing)
- SecurityOnline — Node.js Security Release Analysis
- CybersecurityNews — Node.js TLS, DNS, Brotli Vulnerabilities
- Lilting.ch — CVE Technical Deep Dive and fetch() Attack Vector
- Google Brotli Upstream Fix (PR #505 context)
Why this verdict
- Automatic decompression surface is the multiplier.
fetch()— the standard HTTP client in every modern Node.js app — auto-decompressesContent-Encoding: brresponses without opt-in. The attacker's server chooses the encoding, not the client. This means any outbound HTTP request to an attacker-influenced URL is a trigger, expanding the attack surface far beyond apps that explicitly usezlib.brotliDecompress(). - RCE impact on a massive installed base. Node.js is one of the world's most deployed server-side runtimes. The theoretical worst case is full code execution in the context of the Node.js process, which often runs with access to secrets, databases, and internal APIs. Even with exploitation friction, the consequence tier is HIGH.
- Role multiplier: In *typical web-server / API-backend roles* (~70% of Node.js installs), exploitation requires the app to fetch attacker-controlled URLs — many do (webhooks, previews, SSR data fetching), but many don't. Blast radius is host-level RCE. In *CI/CD roles* (~5–10% of Node.js installs), build processes that fetch external resources during
npm installor build steps could be targeted, with blast radius reaching supply-chain / pipeline compromise. In *SSR framework roles* (Next.js, Nuxt, Remix — significant share of web-tier), server-sidefetch()to external data sources is the core pattern, making the chain highly reachable with host→tenant blast radius. None of these individually trigger the CRITICAL floor because Node.js is a general-purpose runtime, not canonically a high-value-role component (it is not an IdP, hypervisor, PAM, CA, or network edge appliance). But the CI/CD and SSR vectors keep the floor firmly at HIGH. - No weaponized exploit — friction is real but not decisive. The CaterMaoZi research toolkit ran 1,200+ structured fuzzing iterations with zero crashes. The OOB write exists (confirmed by the upstream fix), but converting it to reliable RCE against V8's compacting garbage-collected heap with ASLR is a substantial engineering effort. This prevents escalation to CRITICAL but does not warrant downgrade to MEDIUM given the surface breadth.
- 1-day-old CVE with active researcher interest. The PoC toolkit appeared within 24 hours of disclosure. Exploitation tooling maturity will increase. Assess and act now before the window closes.
Why not higher?
CRITICAL would require either confirmed in-the-wild exploitation, a reliable public exploit, or a canonical high-value-role component. None apply. The OOB write has not been demonstrated to produce even a crash after targeted fuzzing, let alone controlled code execution. The attack requires the victim to initiate an outbound connection to attacker-controlled infrastructure, which is a meaningful prerequisite that limits the reachable population. V8's heap layout and modern OS mitigations (ASLR, NX) create genuine barriers to reliable exploitation.
Why not lower?
MEDIUM would undercount the risk. The fetch() auto-decompression path means the vulnerable code runs in millions of Node.js processes without any developer opt-in — this is not a niche API. RCE is the documented impact class. A research toolkit with fuzzing and delivery-vector tooling appeared within 24 hours, signaling active offensive interest. The installed base spans web servers, API gateways, SSR frameworks, and CI/CD pipelines. Even with exploitation friction, the combination of surface breadth and impact severity demands HIGH.
Crowdsourced verification payload.
Run this script on each host where Node.js is installed. It checks all node binaries found in $PATH and common install locations. Requires no special privileges — just read access to the node binary. Example: chmod +x check_cve_2026_111497.sh && ./check_cve_2026_111497.sh
#!/usr/bin/env bash
# check_cve_2026_111497.sh — Detect Node.js CVE-2026-111497 (Brotli OOB write)
# Run on target hosts. Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
VULNERABLE=0
PATCHED=0
UNKNOWN=0
check_version() {
local node_bin="$1"
local raw_version
raw_version=$( "$node_bin" --version 2>/dev/null ) || { UNKNOWN=$((UNKNOWN+1)); return; }
# Strip leading 'v'
local version="${raw_version#v}"
local major minor patch
IFS='.' read -r major minor patch <<< "$version"
# Affected: 22.x < 22.23.4, 24.x < 24.22.1, 26.x < 26.12.1
# Also affected: any 23.x, 25.x (unsupported, no fix available)
local status="UNKNOWN"
case "$major" in
22)
if [[ "$minor" -lt 23 ]] || { [[ "$minor" -eq 23 ]] && [[ "$patch" -lt 4 ]]; }; then
status="VULNERABLE"
else
status="PATCHED"
fi
;;
24)
if [[ "$minor" -lt 22 ]] || { [[ "$minor" -eq 22 ]] && [[ "$patch" -lt 1 ]]; }; then
status="VULNERABLE"
else
status="PATCHED"
fi
;;
26)
if [[ "$minor" -lt 12 ]] || { [[ "$minor" -eq 12 ]] && [[ "$patch" -lt 1 ]]; }; then
status="VULNERABLE"
else
status="PATCHED"
fi
;;
23|25)
# Unsupported branches — no patch available
status="VULNERABLE"
;;
*)
if [[ "$major" -lt 22 ]]; then
# Node.js < 22 is EOL; Brotli support exists since v10
# but these versions are outside the advisory scope
status="UNKNOWN"
elif [[ "$major" -gt 26 ]]; then
status="PATCHED" # Future versions assumed patched
else
status="UNKNOWN"
fi
;;
esac
echo "[${status}] ${node_bin} -> ${raw_version}"
case "$status" in
VULNERABLE) VULNERABLE=$((VULNERABLE+1)) ;;
PATCHED) PATCHED=$((PATCHED+1)) ;;
*) UNKNOWN=$((UNKNOWN+1)) ;;
esac
}
# Find node binaries
NODE_BINS=( $(which -a node 2>/dev/null || true) )
# Also check common install paths
for d in /usr/local/bin/node /usr/bin/node /opt/nodejs/bin/node "$HOME/.nvm/current/bin/node" "$HOME/.local/bin/node"; do
[[ -x "$d" ]] && NODE_BINS+=("$d")
done
# Deduplicate by resolved path
declare -A SEEN
for bin in "${NODE_BINS[@]}"; do
real=$(readlink -f "$bin" 2>/dev/null || echo "$bin")
if [[ -z "${SEEN[$real]:-}" ]]; then
SEEN["$real"]=1
check_version "$bin"
fi
done
if [[ ${#SEEN[@]} -eq 0 ]]; then
echo "[UNKNOWN] No Node.js installation found on this host."
exit 2
fi
echo ""
echo "Summary: VULNERABLE=$VULNERABLE PATCHED=$PATCHED UNKNOWN=$UNKNOWN"
if [[ $VULNERABLE -gt 0 ]]; then
echo "VULNERABLE"
exit 1
elif [[ $UNKNOWN -gt 0 ]] && [[ $PATCHED -eq 0 ]]; then
echo "UNKNOWN"
exit 2
else
echo "PATCHED"
exit 0
fi