← Back to Feed CACHED · 2026-10-28 07:02:20 · CACHE_KEY CVE-2026-111497
CVE-2026-111497 · CWE-252 · Disclosed 2026-10-22

Google Brotli decoder out-of-bounds write via unchecked block type switch

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

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.

"KEV-listed Brotli decoder OOB write hits Node.js, Python, Envoy, curl — block br encoding or patch now"
02 · The Attack Path

4 steps from start to impact.

STEP 01

Identify Brotli-consuming target

The attacker identifies a target that decompresses Brotli data from an untrusted source. For the server-side path, any Node.js/Express endpoint using body-parser with the default 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.
Conditions required:
  • Target must run software embedding Brotli 1.0.3–1.2.0
  • Target must process Brotli-compressed data from an attacker-reachable source
Where this breaks in practice:
  • If the target only sends compressed data (encoding, not decoding), it is not vulnerable
  • Applications that explicitly disable Brotli decompression or strip Content-Encoding: br at a reverse proxy are immune
Detection/coverage: Nuclei template available (Pentest-Tools, added Oct 25). Version-based scanning via node --version, dpkg -s libbrotli1, pip show brotli. Sysdig Threat Intelligence feed has a Node2Shell entry.
STEP 02

Craft malicious Brotli stream

The attacker constructs a valid Brotli stream that declares one block type, fully consumes it, then forces a block type switch. Because 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.
Conditions required:
  • Knowledge of the Brotli compressed stream format (publicly documented in RFC 7932)
  • For RCE: heap grooming tailored to target architecture and allocator
Where this breaks in practice:
  • 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
Detection/coverage: WAF rules detecting malformed Brotli block-switch patterns (Azure WAF released a managed rule). Falco/Sysdig runtime rule 'Node.js Process Crash Followed by In-Place Program Execution' detects post-exploit behavior.
STEP 03

Deliver payload to decoder

Server-side: The attacker sends an HTTP POST to the target endpoint with 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.
Conditions required:
  • 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)
Where this breaks in practice:
  • 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: br blocks the entire chain
Detection/coverage: HTTP access logs show 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.
STEP 04

Trigger out-of-bounds write and impact

The decoder writes past the allocated buffer, corrupting adjacent heap memory. The guaranteed minimum impact is a process crash (DoS) — every unpatched decoder will abort on the malformed stream. The demonstrated maximum impact is arbitrary code execution with the privileges of the host process, as shown by Imperva against Node.js on arm64. An intermediate outcome is memory disclosure — the attacker can read adjacent heap contents, which may contain session tokens, encryption keys, or other secrets. Against Envoy Proxy, a crash restarts the data-plane process, disrupting all traffic flowing through that mesh node; RCE on Envoy gives the attacker a man-in-the-middle position on the entire service mesh.
Conditions required:
  • Successful delivery of the malformed stream to the decoder
  • For RCE: favorable heap layout (architecture- and runtime-dependent)
Where this breaks in practice:
  • 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
Detection/coverage: Process crash monitoring (systemd watchdog, Kubernetes liveness probes). Application performance monitoring (APM) detects elevated error rates. Host-based detection: unexpected child processes spawned by Node.js or Envoy indicate post-exploitation.
03 · Compensating Control

1
CRITICAL 9.0→LOW 2.5
SEVERITY REDUCED
Block or strip Content-Encoding: br at reverse proxy / WAF / load balancer — Configure your edge proxy (NGINX, HAProxy, Cloudflare, Azure WAF, AWS ALB) to reject or strip 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).
2
CRITICAL 9.0→MEDIUM 5.5
SEVERITY REDUCED
Set inflate: false in Express body-parser — For Node.js Express applications, set 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.
3
CRITICAL 9.0→HIGH 7.5
SEVERITY REDUCED
Remove or upgrade Python brotli / brotlicffi packages — Run 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.
4
CRITICAL 9.0→IGNORE 0.0
SEVERITY REDUCED
Upgrade all Brotli-embedding software to fixed versions — This is the definitive remediation. Upgrade Brotli to 1.0.10/1.1.1/1.2.1, Node.js to 22.23.4/24.22.1/26.12.1, Envoy to 1.36.12+/1.37.8+/1.38.6+/1.39.3+/1.40.0, Python brotli to 1.2.1, brotlicffi to 1.2.1.0, .NET to 8.0.33/9.0.22/10.0.14, Bun to 1.4.4, Git for Windows to 2.56.0.windows.4, Go andybalholm/brotli to 1.2.7. Self-contained and NativeAOT .NET apps must be rebuilt, not just runtime-updated. Complete within 90 days per noisgate remediation SLA for CRITICAL.
What doesn't work
  • 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: br responses even when the request sent Accept-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.
04 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationKEV-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 availabilityMultiple 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.
EPSS0.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 statusListed in CISA Known Exploited Vulnerabilities catalog. Exact addition date between Oct 24–28, 2026.
CVSS vectorCVSS: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 versionsGoogle 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 versionsBrotli 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/exposureWiz: 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 timelineFix 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.
ResearchersDiscovered 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.

  1. Wiz — Broccoli2Shell deep-dive
  2. Rapid7 Vulnerability Database
  3. Debian Security Tracker
  4. lilting.ch — Brotli CVE beyond Node.js (Python, curl, Git)
  5. Aikido — Python packages vulnerable to Node2Shell
  6. Sysdig — Detecting Node2Shell
  7. Azure WAF protection for CVE-2026-111497
  8. Tanium — HTTP2Shell detection and remediation
05 · The Call

Final Verdict
= UNCHANGED to CRITICAL (9.0/10)

Why this verdict

  • Unauthenticated remote, zero-interaction server-side attack: Express body-parser decompresses Content-Encoding: br by 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: identity is 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.

06 · Verification

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).

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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