The universal decompressor everyone forgot they were running just became the universal entry point
CVE-2026-111497 is an out-of-bounds write in the Brotli decoder (c/dec/decode.c) present in Google Brotli versions 1.0.3 through 1.2.0, affecting every platform. The root cause is that DecodeBlockTypeAndLength() returns only success or failure, and the fast path ignores even that return value. When a crafted Brotli stream declares one block type, exhausts it, then triggers a block switch, decoding continues with stale state and writes past the allocated buffer. A remote unauthenticated attacker can exploit this by delivering a malformed Brotli-compressed stream to any software that decompresses it — including Node.js (via node:zlib or fetch()), Express body-parser (default inflate: true), Python requests/urllib3/httpx (if brotli or brotlicffi is installed), Envoy Proxy, Istio, Bun, curl --compressed, Git for Windows, .NET Runtime, and more. Fixed versions are 1.0.10, 1.1.1, and 1.2.1 for upstream Brotli; downstream fixes include Node.js 22.23.4 / 24.22.1 / 26.12.1, Bun 1.4.4, 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, Python brotli 1.2.1, brotlicffi 1.2.1.0, .NET Runtime 8.0.33 / 9.0.22 / 10.0.14, and Git for Windows 2.56.0.windows.4.
The CVSS 4.0 score of 9.2 (Critical) from the GitHub CNA is directionally correct but deserves scrutiny. The DoS path is trivially reliable — any unpatched decoder crashes on the malformed stream — and Imperva's Ron Masas demonstrated actual code execution against the official Node.js 24.22.0 arm64 Docker image. However, that RCE required multiple attempts and worked only on one architecture; ASLR and heap randomization make reliable code execution significantly harder on general-purpose builds. The real danger is the combination of trivial DoS, plausible-but-unreliable RCE, and staggering blast radius: Wiz reports 91% of cloud environments contain at least one vulnerable resource. Brotli decompression is automatic and invisible in most HTTP stacks, meaning teams often don't know they're running it. This is a library-level supply-chain vulnerability masquerading as a single-product bug, and treating it as anything less than CRITICAL risks leaving your mesh proxies, API servers, and CI/CD pipelines exposed to an attacker who only needs to serve a crafted HTTP response or send a single POST.
4 steps from start to impact.
Identify Brotli-consuming target
inflate: true setting decompresses Content-Encoding: br on POST/PUT/PATCH requests — no authentication required. For the client-side path, any Python, Node.js, curl, or Envoy process that fetches from an attacker-controlled URL or processes attacker-supplied compressed content is exposed. Shodan/Censys footprinting of X-Powered-By: Express or Envoy server: headers can identify targets at scale.- Target must run software embedding Brotli 1.0.3–1.2.0
- Target must process Brotli-compressed data from an attacker-reachable source
- If the target only sends compressed data (encoding, not decoding), it is not vulnerable
- Applications that explicitly disable Brotli decompression or strip
Content-Encoding: brat a reverse proxy are immune
node --version, dpkg -s libbrotli1, pip show brotli. Sysdig Threat Intelligence feed has a Node2Shell entry.Craft malicious Brotli stream
DecodeBlockTypeAndLength() returns an error that the fast path ignores, the decoder continues writing with the previous state, overflowing the allocated buffer. Crash-only PoCs appeared on GitHub within ~24 hours of the patch landing (Oct 22, 2026). Sysdig observed 11 GitHub repositories referencing the CVE by the end of day one. The Imperva Red Team separately developed a more advanced RCE payload that succeeded on Node.js 24.22.0 arm64.- Knowledge of the Brotli compressed stream format (publicly documented in RFC 7932)
- For RCE: heap grooming tailored to target architecture and allocator
- Crash-only payloads are trivial to construct; RCE payloads require architecture-specific heap layout knowledge
- Imperva's RCE exploit is not published; reproduction requires independent development
- ASLR, stack canaries, and RELRO mitigate reliable code execution on most production builds
Deliver payload to decoder
Content-Encoding: br and the malicious stream as the body. Express's body-parser calls zlib.brotliDecompress() automatically before application code runs. No cookies, tokens, or API keys are needed. Client-side: The attacker controls a web server that responds with Content-Encoding: br and the payload. When the victim's Python requests, Node.js fetch, curl, or Envoy proxy fetches the URL, the decoder is triggered. Importantly, Python's urllib3 decodes Brotli responses even when the request sent Accept-Encoding: identity — the mere presence of the brotli package enables decompression.- Server path: network reachability to an HTTP endpoint on the target
- Client path: target must fetch from an attacker-controlled URL (webhook callback, URL preview, crawler, redirect)
- Server path has near-zero friction: any public Express API with default body-parser is reachable
- Client path requires a lure mechanism (malicious URL in a webhook, redirect chain, etc.)
- Enterprise WAF or reverse proxy that strips or rejects
Content-Encoding: brblocks the entire chain
Content-Encoding: br on inbound requests (unusual for most API traffic). Azure WAF managed rule for CVE-2026-111497 blocks malicious payloads. IDS can alert on crash/restart loops in Node.js processes.Trigger out-of-bounds write and impact
- Successful delivery of the malformed stream to the decoder
- For RCE: favorable heap layout (architecture- and runtime-dependent)
- DoS is 100% reliable; RCE success rate is low on most architectures
- Envoy crash is automatically recovered by the control plane (Istio pilot), limiting DoS duration
- RCE on hardened containers (read-only filesystem, dropped capabilities) limits post-exploitation options
Content-Encoding: br from inbound requests and — for client-side protection — strip it from upstream responses. Azure WAF has a managed rule specifically for CVE-2026-111497. This breaks the entire attack chain at the network boundary without touching application code. Deploy within 3 days per noisgate mitigation SLA for CRITICAL. Watch for case variations (BR, Br) and multi-value headers (gzip, br).express.json({ inflate: false }) and express.raw({ inflate: false }) to reject compressed request bodies. This eliminates the server-side attack surface without upgrading Node.js. Deploy within 3 days. Note: this only protects inbound request decompression; if the application fetches external URLs with fetch() or node:zlib, those paths remain exposed.pip install --upgrade brotli>=1.2.1 brotlicffi>=1.2.1.0 or, if Brotli support is not needed, pip uninstall brotli brotlicffi. Check transitive dependencies: httpx[brotli], aiohttp, and Scrapy may reinstall them. Removing the package disables automatic Brotli decompression in urllib3/requests. Deploy within 3 days. This only covers the Python vector.- Network IDS/IPS payload signatures — the malicious stream is valid Brotli up to the block-switch boundary; there is no fixed byte pattern that distinguishes it from legitimate compressed data without actually decoding it, creating a chicken-and-egg problem.
- WAF body inspection rules — WAFs that inspect decompressed request bodies must decompress first, which is exactly the operation that triggers the vulnerability. Unless the WAF runs a patched Brotli library itself, deep inspection makes the WAF the victim.
- Disabling Accept-Encoding: br on the client — this does NOT protect against malicious servers. Python urllib3 decompresses
Content-Encoding: brresponses even when the request sentAccept-Encoding: identity. The server controls the encoding, not the client header. - Upgrading only the system libbrotli — Node.js, Bun, Envoy, Python brotli, .NET, and Git for Windows each bundle their own copy of the decoder. Patching the OS-level shared library does not fix statically linked or vendored copies.
The supporting signals.
| In-the-wild exploitation | KEV-listed per intake data. CISA's CVE record listed exploitation as 'none' as of Oct 23; the KEV addition appears to have occurred between Oct 24–28. No named threat actor campaigns at time of assessment, but Sysdig observed 11 GitHub repositories and active researcher interest within 24 hours of disclosure. |
|---|---|
| PoC availability | Multiple crash-only PoCs public within ~1 day of patch (Oct 22). Sysdig counted 11 GitHub repos by EOD Oct 22. Imperva Red Team (Ron Masas) demonstrated full RCE on Node.js 24.22.0 arm64 Docker image — exploit code not published. Pentest-Tools and Nuclei have network detection templates. JFrog repository referenced but not confirmed as PoC. |
| EPSS | 0.55483 (55.5%) — 99th percentile. This places CVE-2026-111497 in the top 1% of all CVEs by exploitation probability. Other sources report 0.33–0.37, reflecting daily fluctuation. |
| KEV status | Listed in CISA Known Exploited Vulnerabilities catalog. Exact addition date between Oct 24–28, 2026. |
| CVSS vector | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N → 9.2 Critical (GitHub CNA). Key: AT:P (Attack Prerequisites Present) reflects the requirement that the target must be decompressing attacker-controlled Brotli data. No CVSS 3.1 vector published. |
| Affected versions | Google Brotli 1.0.3 through 1.2.0 (all platforms). Downstream: Node.js all versions before 22.23.4 / 24.22.1 / 26.12.1; Bun 1.0.21–1.4.3; Envoy 1.36.11 / 1.37.7 / 1.38.5 / 1.39.2 and earlier in each line; Istio 1.29.0–1.29.8 / 1.30.0–1.30.5 / 1.31.0–1.31.1; Python brotli ≤1.2.0 / brotlicffi ≤1.2.0.x; .NET Runtime <8.0.33 / <9.0.22 / <10.0.14; Git for Windows <2.56.0.windows.4; Go andybalholm/brotli <1.2.7. |
| Fixed versions | Brotli 1.0.10 / 1.1.1 / 1.2.1. Node.js 22.23.4 / 24.22.1 / 26.12.1. Bun 1.4.4. 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. Python brotli 1.2.1 / brotlicffi 1.2.1.0. .NET 8.0.33 / 9.0.22 / 10.0.14. Git for Windows 2.56.0.windows.4. Debian: bookworm 1.0.9-2+deb12u1, trixie 1.1.0-2+deb13u1, sid 1.2.0-4.1. Still unpatched: DuckDB, ClickHouse, Apache Arrow, 7-Zip-zstd. |
| Scanning/exposure | Wiz: 91% of cloud environments contain at least one vulnerable resource; 82% have publicly exposed Node.js instances. Pentest-Tools Network Scanner has Nuclei-based detection (added Oct 25). Sysdig Threat Intelligence feed covers Node2Shell. Azure WAF has a managed rule. No GreyNoise or Shodan mass-exploitation tags observed at assessment time. |
| Disclosure timeline | Fix committed upstream Feb 27, 2026. Patches released Oct 21, 2026. CVE published Oct 22, 2026. Crash PoCs on GitHub by Oct 22. Imperva RCE demo reported Oct 23. Pentest-Tools scanner added Oct 25. KEV listing by Oct 28. |
| Researchers | Discovered by Khai Tran and Sylvie Mayer (OpenAI Codex Security). Node.js fixes by Matteo Collina and Antoine du Hamel. RCE exploitation demonstrated by Ron Masas (Imperva Red Team). Python ecosystem analysis by OpenSourceMalware and Aikido. |
Sources.
- Wiz — Broccoli2Shell deep-dive
- Rapid7 Vulnerability Database
- Debian Security Tracker
- lilting.ch — Brotli CVE beyond Node.js (Python, curl, Git)
- Aikido — Python packages vulnerable to Node2Shell
- Sysdig — Detecting Node2Shell
- Azure WAF protection for CVE-2026-111497
- Tanium — HTTP2Shell detection and remediation
Why this verdict
- Unauthenticated remote, zero-interaction server-side attack: Express body-parser decompresses
Content-Encoding: brby default on every POST endpoint. The attacker needs only network reachability — no credentials, no user click, no special timing. This is AV:N/PR:N/UI:N. - Trivially reliable denial of service: Every unpatched Brotli decoder aborts on the malformed block-switch stream. Crash-only payloads were public within 24 hours. Against a fleet of Express APIs, this is a one-packet service outage per process.
- Demonstrated code execution (unreliable but real): Imperva achieved RCE on the official Node.js 24.22.0 arm64 image. While it required multiple attempts and architecture-specific conditions, the proof that the OOB write is exploitable for code execution elevates this above a pure DoS.
- EPSS 55.5% and KEV-listed: The combination of top-1%-percentile exploitation probability and confirmed KEV listing signals that real-world exploitation is either occurring or imminent. This overrides any downward friction adjustments.
- Role multiplier — Envoy Proxy / Istio service mesh (fleet-scale blast radius): Envoy is the canonical data-plane proxy for Istio and similar meshes. It decompresses Brotli from external HTTP traffic. Compromising Envoy gives the attacker a man-in-the-middle position on all mesh traffic — secret exfiltration, lateral movement, traffic manipulation. ≥10% of Envoy installations serve as service mesh data plane by definition. This chain succeeds (Envoy decodes external Brotli → crash or RCE → mesh compromise) and the blast radius is fleet-scale. Verdict floor: CRITICAL.
- Role multiplier — Node.js API / CI/CD tier: Express.js is the dominant Node.js web framework and powers API gateways, webhook receivers, URL preview services, and build tooling. Compromising a webhook endpoint in a CI/CD pipeline yields supply-chain access. While Node.js is not a single canonical high-value role, its pervasiveness across typical and high-value tiers makes the aggregate exposure enormous.
- Role multiplier — Python automation and crawlers: Python with requests/urllib3 + the brotli package automatically decompresses Brotli responses, even when
Accept-Encoding: identityis sent. Automation scripts, crawlers, and monitoring agents that fetch attacker-controlled URLs are vulnerable. In CI/CD and monitoring roles, this is a supply-chain and detection-plane risk. - Installed base scale: Wiz reports 91% of cloud environments contain vulnerable resources. This is not a niche appliance — Brotli is embedded in the HTTP stack of effectively every modern runtime. The blast radius per-vulnerability-instance may vary, but the population is enormous.
Why not higher?
Already assessed at CRITICAL, the highest severity. The score is 9.0 rather than 10.0 because code execution remains unreliable — demonstrated only once, on arm64, requiring multiple attempts — and ASLR, heap randomization, and container hardening significantly raise the RCE bar on general-purpose production builds. The CVSS 4.0 AT:P (Attack Prerequisites Present) correctly notes that the target must be actively decompressing attacker-controlled Brotli, which, while very common, is not universal.
Why not lower?
KEV listing and 55.5% EPSS confirm this is not theoretical. The DoS path requires zero sophistication — a single malformed POST crashes any unpatched Express endpoint. Envoy Proxy, a canonical service mesh data plane component (≥10% of installs in the high-value role), is directly affected with fleet-scale blast radius on exploitation. Downgrading below CRITICAL would require evidence that <1% of Envoy/Node.js deployments process external Brotli, but Wiz's 91% cloud exposure figure and the fact that Brotli is enabled by default in every major HTTP stack refute this.
Crowdsourced verification payload.
Run this script on each target host (Linux/macOS) as any unprivileged user. It checks system libbrotli, Node.js, Python brotli/brotlicffi, and Envoy versions against known-fixed releases. Usage: bash check_cve_2026_111497.sh — exits 0 (PATCHED), 1 (VULNERABLE), or 2 (UNKNOWN if no Brotli components detected).
#!/usr/bin/env bash
# CVE-2026-111497 - Brotli Decoder OOB Write - Host Vulnerability Check
# Exit: 0=PATCHED 1=VULNERABLE 2=UNKNOWN (no Brotli found)
set -uo pipefail
OVERALL="PATCHED"; FOUND=0
mark_vuln() { echo "[VULN] $1"; OVERALL="VULNERABLE"; }
mark_ok() { echo "[ OK ] $1"; }
mark_skip() { echo "[SKIP] $1"; }
# Version comparison: returns 0 if $1 >= $2
ver_gte() { [ "$(printf '%s\n%s' "$1" "$2" | sort -V | head -n1)" = "$2" ]; }
# Check Brotli library version against fix tracks
check_brotli() {
local v="$1" label="$2"
local M m p; IFS='.' read -r M m p <<< "$v"
p="${p%%[^0-9]*}"; m="${m%%[^0-9]*}"; M="${M%%[^0-9]*}"
[[ -z "$M" || -z "$m" ]] && { mark_skip "$label v$v (unparseable)"; return; }
FOUND=1
if [[ $M -ne 1 ]]; then mark_ok "$label v$v (outside 1.x)"; return; fi
case $m in
0) if [[ ${p:-0} -ge 10 ]]; then mark_ok "$label v$v (>=1.0.10)";
elif [[ ${p:-0} -ge 3 ]]; then mark_vuln "$label v$v (1.0.3-1.0.9)";
else mark_ok "$label v$v (<1.0.3, not affected)"; fi ;;
1) if [[ ${p:-0} -ge 1 ]]; then mark_ok "$label v$v (>=1.1.1)";
else mark_vuln "$label v$v (1.1.0)"; fi ;;
2) if [[ ${p:-0} -ge 1 ]]; then mark_ok "$label v$v (>=1.2.1)";
else mark_vuln "$label v$v (1.2.0)"; fi ;;
*) mark_ok "$label v$v (>1.2.x)" ;;
esac
}
echo "=== CVE-2026-111497 Brotli Decoder Check ==="
echo ""
# 1. System libbrotli (deb/rpm)
echo "--- System libbrotli ---"
SYS_V=""
if command -v dpkg-query &>/dev/null; then
for pkg in libbrotli1 libbrotlidec1 brotli; do
SYS_V=$(dpkg-query -W -f='\${Version}' "$pkg" 2>/dev/null || true)
[[ -n "$SYS_V" ]] && break
done
if [[ -n "$SYS_V" ]]; then
UP="${SYS_V#*:}"; UP="${UP%%-*}"; UP="${UP%%+*}"
check_brotli "$UP" "libbrotli (deb: $SYS_V)"
else mark_skip "libbrotli not found via dpkg"; fi
elif command -v rpm &>/dev/null; then
SYS_V=$(rpm -q --qf '%{VERSION}' brotli 2>/dev/null || true)
if [[ -n "$SYS_V" && "$SYS_V" != *"not installed"* ]]; then
check_brotli "$SYS_V" "brotli (rpm)"
else mark_skip "brotli not found via rpm"; fi
else mark_skip "No deb/rpm package manager found"; fi
# 2. Node.js
echo ""
echo "--- Node.js ---"
if command -v node &>/dev/null; then
NV=$(node -v 2>/dev/null | sed 's/^v//'); FOUND=1
NM=$(echo "$NV" | cut -d. -f1)
case "$NM" in
22) ver_gte "$NV" "22.23.4" && mark_ok "Node.js v$NV (>=22.23.4)" || mark_vuln "Node.js v$NV (<22.23.4)" ;;
24) ver_gte "$NV" "24.22.1" && mark_ok "Node.js v$NV (>=24.22.1)" || mark_vuln "Node.js v$NV (<24.22.1)" ;;
26) ver_gte "$NV" "26.12.1" && mark_ok "Node.js v$NV (>=26.12.1)" || mark_vuln "Node.js v$NV (<26.12.1)" ;;
*) if [[ $NM -lt 22 ]]; then mark_vuln "Node.js v$NV (EOL, no fix)"
elif [[ $((NM % 2)) -eq 1 ]]; then mark_vuln "Node.js v$NV (odd release, likely unfixed)"
else mark_skip "Node.js v$NV (check vendor advisory)"; fi ;;
esac
else mark_skip "Node.js not installed"; fi
# 3. Python brotli / brotlicffi
echo ""
echo "--- Python brotli ---"
for PY in python3 python; do
command -v "$PY" &>/dev/null || continue
for mod in brotli brotlicffi; do
PV=$("$PY" -I -c "import ${mod}; print(${mod}.__version__)" 2>/dev/null || true)
[[ -n "$PV" ]] && check_brotli "$PV" "$mod ($PY)"
done
done
# 4. Envoy
echo ""
echo "--- Envoy Proxy ---"
if command -v envoy &>/dev/null; then
EV=$(envoy --version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
if [[ -n "$EV" ]]; then
FOUND=1; EM=$(echo "$EV" | cut -d. -f1,2)
case "$EM" in
1.36) ver_gte "$EV" "1.36.12" && mark_ok "Envoy v$EV" || mark_vuln "Envoy v$EV (<1.36.12)" ;;
1.37) ver_gte "$EV" "1.37.8" && mark_ok "Envoy v$EV" || mark_vuln "Envoy v$EV (<1.37.8)" ;;
1.38) ver_gte "$EV" "1.38.6" && mark_ok "Envoy v$EV" || mark_vuln "Envoy v$EV (<1.38.6)" ;;
1.39) ver_gte "$EV" "1.39.3" && mark_ok "Envoy v$EV" || mark_vuln "Envoy v$EV (<1.39.3)" ;;
*) ver_gte "$EV" "1.40.0" && mark_ok "Envoy v$EV" || mark_skip "Envoy v$EV (verify manually)" ;;
esac
fi
else mark_skip "Envoy not installed"; fi
# 5. Bun
echo ""
echo "--- Bun ---"
if command -v bun &>/dev/null; then
BV=$(bun --version 2>/dev/null || true); FOUND=1
ver_gte "$BV" "1.4.4" && mark_ok "Bun v$BV (>=1.4.4)" || mark_vuln "Bun v$BV (<1.4.4)"
else mark_skip "Bun not installed"; fi
echo ""
echo "================================"
if [[ $FOUND -eq 0 ]]; then
echo "UNKNOWN - no Brotli components detected on this host"
exit 2
elif [[ "$OVERALL" == "VULNERABLE" ]]; then
echo "VULNERABLE - one or more components need patching for CVE-2026-111497"
exit 1
else
echo "PATCHED - all detected Brotli components are at fixed versions"
exit 0
fi