CVE-2026-111497: Audit Brotli Exposure Across Node.js and Envoy

Malformed Brotli stream contained at a decompression boundary before reaching network services

CVE-2026-111497 is an out-of-bounds write in the Brotli decoder that can turn a compressed data stream into a memory-corruption event. The bug matters because Brotli is not only a standalone library. It is bundled or consumed by runtimes, proxies, HTTP clients, security appliances, and language packages, so a single dependency flaw can appear at several points in the same request path.

The practical response is not to assume that every system with a Brotli package is remotely exploitable. Teams need to identify which processes actually decompress untrusted Brotli input, map the implementation and version behind each process, and then apply the vendor-specific fix. This guide provides that exposure-first workflow and a safe offline lab for triaging an inventory without sending malicious data.

What CVE-2026-111497 changes

The vulnerability is in the decoder’s block-type switch handling. A malformed compressed stream can cause a write outside the intended internal buffer, corrupting adjacent decoder state. Depending on the host process and memory layout, the result may be a crash, disclosure of process memory, or potentially arbitrary code execution.

Google’s Brotli project published version 1.2.1 with the security fix on October 21, 2026. The upstream advisory covers Brotli versions 1.0.3 through 1.2.0 and gives branch-specific destinations: 1.0.10, 1.1.1, or 1.2.1. That detail matters for distributions or products that maintain an older branch rather than moving directly to 1.2.1.

Node.js disclosed the same flaw as a high-severity issue in the Brotli decoder bundled with the runtime. Its October 21 security releases fixed the supported 22.x, 24.x, and 26.x lines in versions 22.23.4, 24.22.1, and 26.12.1. Envoy’s October 23 advisory identifies vulnerable releases 1.36.11, 1.37.7, 1.38.5, and 1.39.2, with corresponding fixed releases 1.36.12, 1.37.8, 1.38.6, 1.39.3, and 1.40.0.

Exposure is about data flow, not package presence

The precondition is important: a vulnerable decoder must process a Brotli stream controlled by an attacker. Common exposure paths include an HTTP client that automatically decodes a response with Content-Encoding: br, a Node.js application that passes untrusted data to node:zlib, or an Envoy listener configured with the Brotli decompressor. An installed library that never receives untrusted compressed input is still a patching concern, but it is not equivalent to a directly reachable service.

Red Hat rates the issue Important and notes that there was no public, reliable remote-code-execution proof of concept in its October 23 assessment. That is a reason to avoid sensational claims, not a reason to delay. Memory corruption remains dangerous, crashes are a credible outcome, and public understanding can change quickly after disclosure.

Affected components and published fixes

ComponentPublished affected scopePublished fixed destinationExposure question
Google Brotli decoder1.0.3 through 1.2.01.0.10, 1.1.1, or 1.2.1 by branchWhich reachable process loads or embeds it?
Node.jsSupported 22.x, 24.x, and 26.x before the October 21 releases22.23.4, 24.22.1, or 26.12.1Can an external peer supply data that the process decompresses?
Envoy1.36.11, 1.37.7, 1.38.5, and 1.39.21.36.12, 1.37.8, 1.38.6, 1.39.3, or 1.40.0Is the Brotli decompressor configured on an exposed path?
PyPI BrotliPackage versions through 1.2.01.2.1Does an HTTP or application dependency auto-decode Brotli?
PAN-OS and Prisma Cloud clientSelected branches and client versions in the vendor matrixUse the current Palo Alto Networks advisoryIs the feature enabled, and is a branch-specific hotfix available?

Palo Alto Networks published a product-impact matrix on October 26 and was still updating remediation availability on October 28. The advisory lists affected PAN-OS 11.2, 12.1, and 12.2 branches and Prisma Cloud Application Security Transporter Client versions before 0.2.9. It also says PAN-OS 10.2 and 11.1 are unaffected. Because some hotfixes had future availability dates, operators should use the live vendor matrix rather than copy a static version table into a change ticket.

A defensible triage workflow

1. Find decoding boundaries

Start with traffic flow. List internet-facing reverse proxies, API gateways, service-mesh sidecars, application runtimes, webhook consumers, update agents, browsers, and outbound HTTP clients. Mark where each component accepts or automatically decodes Brotli. This produces a better priority list than a flat software inventory because it distinguishes reachable decoding from dormant dependency presence.

node --version
node -p "process.versions.brotli"

envoy --version

python3 -m pip show Brotli
python3 -m pip show brotlicffi

dpkg-query -W 'libbrotli*' 2>/dev/null
rpm -qa | grep -i '^brotli' 2>/dev/null

Run the commands inside the same container, virtual environment, or host namespace as the exposed process. A safe host package does not prove that a statically linked binary, container image, language wheel, or bundled runtime is safe.

2. Confirm the implementation behind the feature

Do not infer the decoder version only from the application name. Node.js bundles Brotli. Python’s Brotli package includes the C decoder. Envoy publishes its own release mapping. Appliances may ship a patched copy under a vendor hotfix. Record the product version, the embedded or linked decoder when visible, the deployment image digest, and the configuration that enables decompression.

3. Patch in exposure order

  1. Externally reachable processes that automatically decompress attacker-controlled responses or requests.
  2. Shared gateways and proxies where one crash can disrupt many services.
  3. Privileged services or processes holding secrets that could be exposed by memory disclosure.
  4. Internal clients that retrieve data from partially trusted or redirectable destinations.
  5. Installed but currently non-decoding components, which still need normal patch closure.

If immediate patching is impossible, disable Brotli decoding where the product supports that change, restrict who can reach the decoding path, and prefer trusted upstreams. Treat these steps as temporary risk reduction. They do not repair the decoder, and configuration changes should be regression-tested for content negotiation and availability.

Hands-on lab: triage a mixed inventory offline

This lab is intentionally non-exploitative. It compares a CSV inventory with the fixed releases published by Node.js, Envoy, and PyPA. It makes no network requests and sends no compressed payloads.

Prerequisites and isolated environment

  • Python 3.9 or later.
  • An empty working directory on an analyst workstation or disposable VM.
  • A CSV built from approved asset-inventory data. Do not include credentials or secrets.

Create inventory.csv:

asset,component,version
web-api,nodejs,22.23.3
worker,nodejs,22.23.4
edge-proxy,envoy,1.39.2
mesh-gateway,envoy,1.39.3
python-client,python-brotli,1.2.0
toolkit,python-brotli,1.2.1
firewall,pan-os,12.2.3

Save the following as brotli_inventory.py:

#!/usr/bin/env python3
import csv, sys

FIXED = {
    "nodejs": {"22": "22.23.4", "24": "24.22.1", "26": "26.12.1"},
    "envoy": {
        "1.36": "1.36.12", "1.37": "1.37.8", "1.38": "1.38.6",
        "1.39": "1.39.3", "1.40": "1.40.0"
    },
    "python-brotli": {"1": "1.2.1"},
}

def vt(value):
    try:
        return tuple(int(x) for x in value.lstrip("v").split("."))
    except ValueError:
        return None

def branch(component, version):
    parts = version.lstrip("v").split(".")
    if component == "nodejs": return parts[0]
    if component == "envoy": return ".".join(parts[:2])
    if component == "python-brotli": return "1"
    return None

print("asset,component,version,status,detail")
with open(sys.argv[1], newline="", encoding="utf-8") as handle:
    for row in csv.DictReader(handle):
        component = row["component"].lower()
        fixed = FIXED.get(component, {}).get(branch(component, row["version"]))
        parsed = vt(row["version"])
        if not fixed or not parsed:
            status, detail = "MANUAL_REVIEW", "unknown or unsupported branch"
        elif parsed < vt(fixed):
            status, detail = "UPDATE", f"fixed release for this branch: {fixed}"
        else:
            status, detail = "CURRENT", f"meets published floor: {fixed}"
        print(f'{row["asset"]},{component},{row["version"]},{status},{detail}')

Run it without installing any packages:

python3 brotli_inventory.py inventory.csv

Expected output

asset,component,version,status,detail
web-api,nodejs,22.23.3,UPDATE,fixed release for this branch: 22.23.4
worker,nodejs,22.23.4,CURRENT,meets published floor: 22.23.4
edge-proxy,envoy,1.39.2,UPDATE,fixed release for this branch: 1.39.3
mesh-gateway,envoy,1.39.3,CURRENT,meets published floor: 1.39.3
python-client,python-brotli,1.2.0,UPDATE,fixed release for this branch: 1.2.1
toolkit,python-brotli,1.2.1,CURRENT,meets published floor: 1.2.1
firewall,pan-os,12.2.3,MANUAL_REVIEW,unknown or unsupported branch

MANUAL_REVIEW is deliberate. Vendor appliances, distribution backports, unsupported branches, and custom builds should be checked against their current vendor advisory instead of being forced through generic semantic-version logic.

Troubleshooting and cleanup

  • If Python reports missing columns, confirm the header is exactly asset,component,version.
  • If a version contains distribution suffixes, normalize it in the CSV or mark it for manual review and consult the distribution advisory.
  • If a component is statically linked, obtain its build manifest or software bill of materials before declaring it current.
rm -f inventory.csv brotli_inventory.py

Verification after the upgrade

Rebuild images and redeploy workloads rather than patching only a running container. Then repeat the runtime-level checks, confirm the new image digest or package build, and exercise normal Brotli content negotiation with a trusted endpoint. Validate both successful decoding and ordinary error handling. Do not use public proof-of-concept payloads in production.

Watch for unexplained process exits, segmentation faults, decoder errors, restarts, or sanitizer alerts around services that accept compressed data. These events are not unique indicators of CVE-2026-111497, so correlate them with request metadata, deployment versions, and crash telemetry. Preserve relevant logs and core dumps under your incident-response policy if suspicious failures occurred before patching.

Network defenders should also confirm that monitoring survived the change. The Suricata upgrade validation guide provides a practical pattern for checking rule loading and EVE telemetry after a security update. Teams managing Palo Alto Networks devices can pair the live product matrix with the site’s PAN-OS patch-response workflow for management-plane safeguards and evidence collection.

Conclusion

CVE-2026-111497 is a shared-component problem with product-specific fixes. The fastest responsible response is to map decoding boundaries, identify the actual implementation in each process, patch exposed services first, and verify the deployed runtime rather than the build manifest alone. Keep temporary mitigations narrow and time-limited, and revisit vendor advisories that are still changing.

Primary sources

Similar Posts