Skip to main content

Security advisory · CVE-2026-111497 · Node2Shell

HTTP2Shell

Severity · CriticalCVSS v4.0 · 9.2Brotli · Node.js · Python

Published

Google's C Brotli decoder, versions 1.0.3 through 1.2.0, writes past an 8-byte internal buffer when a crafted stream forces an impossible block switch. The result is a crash, a leak of process memory, or code execution. It was disclosed on 2026-10-21 as "Node2Shell", and is now called "HTTP2Shell" because every HTTP stack that decodes Brotli is affected.

The attacker needs no account. A Brotli request body reaches the decoder in Express 5, body-parser 2, Envoy and Istio; a Brotli response reaches it in Node's fetch() and in Python's HTTP clients whenever the Brotli package is installed. No exploitation in the wild had been reported on 2026-10-22.

Impact
Unauthenticated crash, memory leak or code execution
Affected
Brotli 1.0.3 – 1.2.0 and everything that bundles it
Fixed in
Brotli 1.2.1 · Node.js 24.22.1 · Python Brotli 1.2.1
.NET
No fixed runtime released yet
Disclosed

[01]

Two ways in

Inbound

Brotli request bodies

A request sent with Content-Encoding: br is inflated before your code sees it. Express 5.0 to 5.3 and body-parser 2.0 to 2.3 were tested and do this, and so do Envoy and Istio.

Outbound

Brotli responses

A server you do not control answers a webhook, URL preview, crawler or other URL fetch with Brotli. Node's fetch() decodes it, and so do urllib3, requests, aiohttp, httpx and Scrapy whenever the Python Brotli or brotlicffi package is installed. Your code never has to import Brotli.

[A]Timeline

  1. 10-21

    Disclosed as Node2Shell. Node.js ships 22.23.4, 24.22.1 and 26.12.1. Upstream Brotli's fix is commit 0ed4268.

  2. 10-22

    Python Brotli 1.2.1 and brotlicffi 1.2.1.0 ship; GitHub publishes GHSA-9v8h-qqgm-4m35 for them.

  3. 10-23

    Envoy publishes GHSA-3wvj-j6qm-62mr; Aikido reports the wider impact on Envoy, Istio, Bun, curl and .NET. Go's andybalholm/brotli fixes its crash-only variant.

[B]Not affected

  • Rust's brotli-decompressor, so Deno and Swamp itself, which is built on Deno.
  • The Java, JavaScript and C# ports.
  • Go's andybalholm/brotli has the same logic error but can only crash, and was fixed on 2026-10-23.

[C]Scanners missed it

On 2026-10-23, pip-audit, Dependabot and Safety did not flag the affected PyPI versions. The scanner below checks versions itself instead of relying on them.

Fixed versions

ComponentFixed in
Google Brotli (C), 1.0.3 – 1.2.0 affected1.0.10, 1.1.1, 1.2.1
Node.js 22 / 24 / 2622.23.4, 24.22.1, 26.12.1
Node.js 20, 23, 25 and olderno fix (end of life)
Bun1.4.4
Python Brotli1.2.1
Python brotlicffi1.2.1.0
Envoy1.37.8, 1.38.6, 1.39.3, 1.40.0; 1.36.12 (out of support)
Istio1.30.6, 1.31.2; 1.29.9 (end of life)
Ruby brotli gem0.9.1
.NET runtime (bundles Brotli 1.1.0)no fix released yet

Distributions backport the fix without changing the upstream version, so a distro build below these numbers may already be fixed. The tracker entries the scanner knows, as of 2026-10-24 18:00 UTC:

DistroPackageStatus
Debian 12 (bookworm)libbrotli1, python3-brotlifixed in 1.0.9-2+deb12u1
Debian 13 (trixie)libbrotli1, python3-brotlifixed in 1.1.0-2+deb13u1
Debian forky / sidlibbrotli1, python3-brotli1.2.0-4 and older affected, no fix yet
Amazon Linux 2023libbrotli, brotliaffected, fix pending
Amazon Linux 2023nodejs24affected, fix pending
Amazon Linux 2023nodejs22not affected
Amazon Linux 2023nodejs, nodejs20no fix planned

Any other distro build below the upstream fix (Ubuntu, RHEL, Fedora, Alpine, SUSE) is reported as check-vendor, never guessed either way.

Stopgaps

Until everything is patched, reject Brotli request bodies before they reach the decoder:

  • nginx: if ($http_content_encoding ~* "(^|,)\s*br\s*(,|$)") { return 415; }
  • HAProxy: http-request deny deny_status 415 if { req.hdr(content-encoding) -m reg -i (^|,)\s*br\s*(,|$) }
  • Envoy: a route that matches the content-encoding header with the same regex and a direct_response of 415, ahead of the Brotli decompressor filter.

For outbound requests, removing br from Accept-Encoding is not enough: a malicious server can send Brotli anyway. Reject responses whose Content-Encoding includes br before the client decodes them, or uninstall the Python Brotli and brotlicffi packages.

[02]

Check and fix with Swamp

  1. Step 1:Am I affected?

    A host is likely exposed if any of these is true.

    Upgrading is not the end: a process keeps running the old library until it is restarted.

    • It runs Node.js below 22.23.4, 24.22.1 or 26.12.1 for its line, or any release of Node.js 20, 23 or 25.
    • Any Python environment has Brotli below 1.2.1 or brotlicffi below 1.2.1.0 installed, even if no code imports it.
    • It runs Envoy, Istio, Bun or the Ruby brotli gem below the fixed versions, or a .NET runtime.
    • Its system Brotli library, which distro Node.js, curl and .NET link, predates the distro's fix.
    • It runs container images built before 2026-10-21.
  2. Step 2:Install Swamp and the scanner

    The @swamp/cve/http2shell extension finds every Brotli decoder on a host: the system library, Node.js, Bun, Python Brotli and brotlicffi, .NET, Envoy, Istio, curl, the Ruby gem, Electron apps, lockfile pins, and processes still running old code.

    Each gets a verdict and an action. scan needs no root and never uses sudo.

    Install Swamp:

    curl -fsSL https://swamp-club.com/install.sh | sh

    Initialize a new repo (run this in a new empty directory):

    swamp repo init

    Pull the scanner:

    swamp extension pull @swamp/cve/http2shell

    Create the scanner, with the SSH login it uses for remote hosts and the directories to search for lockfiles and virtualenvs:

    swamp behavior create @swamp/cve/http2shell brotli-fleet \
        --global-arg sshUser=ubuntu \
        --global-arg sshKey=~/.ssh/id_ed25519 \
        --global-arg searchPaths=/srv,/opt/apps

    Am I affected (this machine):

    swamp behavior run brotli-fleet scan

    Mitigate (a dry run unless you pass dryRun=false):

    swamp behavior run brotli-fleet mitigate

    Scan a running container, or an image:

    swamp behavior create @swamp/cve/http2shell brotli-api \
        --global-arg dockerContainer=api
    swamp behavior create @swamp/cve/http2shell brotli-img \
        --global-arg dockerImage=gcr.io/distroless/nodejs24-debian12
    swamp behavior run brotli-img scan

    An image is started with --network none while it is scanned. Distroless images have no sh, so the scanner runs their own node, bun and python3 directly, and lists what it could not check.

    ›Image scan results

    A real scan of gcr.io/distroless/nodejs24-debian12 as pulled on 2026-10-25: it ships Node.js 24.14.0.

    # Brotli Decoder Scan (CVE-2026-111497, Node2Shell / HTTP2Shell)
    
    1 target(s) scanned: 1 with affected decoders, 0 with only decoders to check
    with the vendor, 0 patched or without Brotli
    
    ┌─────────────────────────────────────┬──────────┬──────────┬──────────────┬────────────┬──────┬───────────────┐
    │ Target                              │ Decoders │ Affected │ Check vendor │ Worst      │ Risk │ Action        │
    ├─────────────────────────────────────┼──────────┼──────────┼──────────────┼────────────┼──────┼───────────────┤
    │ gcr.io/distroless/nodejs24-debian12 │ 1        │ 1        │ 0            │ vulnerable │ HIGH │ rebuild-image │
    └─────────────────────────────────────┴──────────┴──────────┴──────────────┴────────────┴──────┴───────────────┘
    
    ## Affected decoders
    
    ┌─────────────────────────────────────┬──────┬──────────────────┬─────────┬────────────┬──────────┬───────────────┐
    │ Target                              │ Kind │ Path             │ Version │ Verdict    │ Fixed in │ Action        │
    ├─────────────────────────────────────┼──────┼──────────────────┼─────────┼────────────┼──────────┼───────────────┤
    │ gcr.io/distroless/nodejs24-debian12 │ node │ /nodejs/bin/node │ 24.14.0 │ vulnerable │ 24.22.1  │ rebuild-image │
    └─────────────────────────────────────┴──────┴──────────────────┴─────────┴────────────┴──────────┴───────────────┘
    
    ## Recommendations
    
        * Rebuild this image on a base and runtime with the fix (Node.js 22.23.4, 24.22.1 or 26.12.1; Brotli 1.2.1 / brotlicffi 1.2.1.0; patched distro brotli) and redeploy; do not patch running containers.
    
    ## Not checked
    
        * gcr.io/distroless/nodejs24-debian12: The image has no sh: only its node, bun and python runtimes were checked. OS packages, .NET, curl, lockfiles and processes were not.
    
    ## Stopgaps until everything is patched
    
        * nginx: reject Brotli request bodies before they reach the app: if ($http_content_encoding ~* "(^|,)\s*br\s*(,|$)") { return 415; }
        * HAProxy: http-request deny deny_status 415 if { req.hdr(content-encoding) -m reg -i (^|,)\s*br\s*(,|$) }
        * Envoy: add a route that matches header content-encoding with safe_regex (?i)(^|,)\s*br\s*(,|$) and a direct_response of 415, ahead of the Brotli decompressor filter.
        * Outbound: removing br from Accept-Encoding is not enough, because a malicious server can send Brotli anyway; reject responses whose Content-Encoding includes br before the client decodes them, or uninstall the Python Brotli and brotlicffi packages (clients fall back to gzip and deflate).
    
    Fixed upstream: Brotli 1.0.10, 1.1.1, 1.2.1; Node.js 22.23.4, 24.22.1, 26.12.1;
    Python Brotli 1.2.1, brotlicffi 1.2.1.0. Distro verdicts use the tracker
    snapshot from 2026-10-24T18:00Z. No indicators of compromise have been
    published.
  3. Step 3:Scan your fleet

    Pass a comma-separated list of hosts and scanFleet scans Linux, macOS and Windows hosts in parallel over SSH. A host that needs a different login takes it as user@host; plain entries use sshUser, and every host uses sshKey.

    Windows hosts are scanned with a PowerShell probe that needs no administrator rights. A host it cannot reach is reported as an error, never as clean.

    swamp behavior run brotli-fleet scanFleet \
        --input hosts=ubuntu@192.0.2.10,Administrator@192.0.2.11,192.0.2.12

    ›Scan results

    A real scan of a container named shop-api built on node:24.22.0-bookworm-slim, with an Express 5 app in /srv/api and a Python worker virtualenv on Brotli 1.2.0 in /srv/worker, scanned with searchPaths=/srv:

    # Brotli Decoder Scan (CVE-2026-111497, Node2Shell / HTTP2Shell)
    
    1 target(s) scanned: 1 with affected decoders, 0 with only decoders to check
    with the vendor, 0 patched or without Brotli
    
    ┌─────────────────┬──────────┬──────────┬──────────────┬────────────┬──────┬───────────────┐
    │ Target          │ Decoders │ Affected │ Check vendor │ Worst      │ Risk │ Action        │
    ├─────────────────┼──────────┼──────────┼──────────────┼────────────┼──────┼───────────────┤
    │ docker-shop-api │ 2        │ 2        │ 0            │ vulnerable │ HIGH │ rebuild-image │
    └─────────────────┴──────────┴──────────┴──────────────┴────────────┴──────┴───────────────┘
    
    ## Affected decoders
    
    ┌─────────────────┬────────┬───────────────────────────────────────────────┬─────────┬────────────┬──────────┬───────────────┐
    │ Target          │ Kind   │ Path                                          │ Version │ Verdict    │ Fixed in │ Action        │
    ├─────────────────┼────────┼───────────────────────────────────────────────┼─────────┼────────────┼──────────┼───────────────┤
    │ docker-shop-api │ node   │ /usr/local/bin/node                           │ 24.22.0 │ vulnerable │ 24.22.1  │ rebuild-image │
    ├─────────────────┼────────┼───────────────────────────────────────────────┼─────────┼────────────┼──────────┼───────────────┤
    │ docker-shop-api │ python │ /srv/worker/venv/lib/python3.11/site-packages │ 1.2.0   │ vulnerable │ 1.2.1    │ rebuild-image │
    └─────────────────┴────────┴───────────────────────────────────────────────┴─────────┴────────────┴──────────┴───────────────┘
    
    ## Exposure
    
        * docker-shop-api: /srv/api/package-lock.json: body-parser 2.4.0 inflates Brotli request bodies into Node's decoder
        * docker-shop-api: /srv/api/package-lock.json: express 5.1.0 inflates Brotli request bodies into Node's decoder
        * docker-shop-api: requests in /srv/worker/venv/lib/python3.11/site-packages decodes Brotli responses automatically
        * docker-shop-api: urllib3 in /srv/worker/venv/lib/python3.11/site-packages decodes Brotli responses automatically
    
    ## Recommendations
    
        * Rebuild this image on a base and runtime with the fix (Node.js 22.23.4, 24.22.1 or 26.12.1; Brotli 1.2.1 / brotlicffi 1.2.1.0; patched distro brotli) and redeploy; do not patch running containers.
    
    ## Stopgaps until everything is patched
    
        * nginx: reject Brotli request bodies before they reach the app: if ($http_content_encoding ~* "(^|,)\s*br\s*(,|$)") { return 415; }
        * HAProxy: http-request deny deny_status 415 if { req.hdr(content-encoding) -m reg -i (^|,)\s*br\s*(,|$) }
        * Envoy: add a route that matches header content-encoding with safe_regex (?i)(^|,)\s*br\s*(,|$) and a direct_response of 415, ahead of the Brotli decompressor filter.
        * Outbound: removing br from Accept-Encoding is not enough, because a malicious server can send Brotli anyway; reject responses whose Content-Encoding includes br before the client decodes them, or uninstall the Python Brotli and brotlicffi packages (clients fall back to gzip and deflate).
    
    Fixed upstream: Brotli 1.0.10, 1.1.1, 1.2.1; Node.js 22.23.4, 24.22.1, 26.12.1;
    Python Brotli 1.2.1, brotlicffi 1.2.1.0. Distro verdicts use the tracker
    snapshot from 2026-10-24T18:00Z. No indicators of compromise have been
    published.
  4. Step 4:Mitigate across your fleet

    mitigate scans first. On Linux it upgrades every installed Brotli library and binding package, and any affected distro Node.js, through apt, dnf, yum or apk, restarts the systemd units still running the old library, then scans again.

    Containers are reported for an image rebuild and never changed in place. macOS and Windows hosts get a saved script with the exact commands.

    dryRun defaults to true — the method shows the plan and changes nothing:

    swamp behavior run brotli-fleet mitigate \
        --input hosts=ubuntu@192.0.2.10,192.0.2.12

    Pass dryRun=false to apply:

    swamp behavior run brotli-fleet mitigate \
        --input hosts=ubuntu@192.0.2.10,192.0.2.12 \
        --input dryRun=false

    Add upgradePython=true to also pip-upgrade Brotli and brotlicffi in the virtualenvs the scanning user owns. Another user's environment is reported with the command for its owner to run, never touched:

    swamp behavior run brotli-fleet mitigate \
        --input hosts=ubuntu@192.0.2.10 \
        --input dryRun=false \
        --input upgradePython=true

    Mitigation needs root, or passwordless sudo for the exact commands the dry run prints. If sudo would ask for a password, the host is reported as needs-privilege and nothing is changed. Upstream Node.js, Bun, Envoy and .NET are reported with the command to run.

    ›Mitigation results

    A dry run on the same shop-api container: mitigate never changes a container, and reports the image to rebuild.

    # Brotli (CVE-2026-111497) Mitigation: DRY RUN
    
    ┌─────────────────┬──────────────────┬────────────┬──────────┬───────────────┐
    │ Target          │ Outcome          │ Privilege  │ Risk now │ Action        │
    ├─────────────────┼──────────────────┼────────────┼──────────┼───────────────┤
    │ docker-shop-api │ no-automatic-fix │ not-needed │ HIGH     │ rebuild-image │
    └─────────────────┴──────────────────┴────────────┴──────────┴───────────────┘
    
    ## docker-shop-api
    
        * Target is a container: rebuild its image on a fixed base and runtime instead of changing it in place.

    The script mitigate saves for a Mac, generated by @swamp/cve/http2shell 2026.10.24.1 for a Mac with Homebrew Brotli 1.2.0, Homebrew Node.js 24, an nvm Node.js 24.22.0 and a project virtualenv on Brotli 1.2.0. Homebrew's Node.js links Homebrew's Brotli, so upgrading Brotli fixes both:

    #!/bin/sh
    # CVE-2026-111497 (Brotli decoder RCE): fixes for this Mac, from @swamp/cve/http2shell.
    set -e
    brew upgrade brotli
    # /Users/dev/.nvm/versions/node/v24.22.0/bin/node: Node.js 24.22.0; install 24.22.1 with the tool that installed it
    /Users/dev/src/crawler/.venv/bin/python3.12 -I -m pip install --upgrade 'Brotli>=1.2.1' 'brotlicffi>=1.2.1.0'
    # Then restart the apps and services that use Node.js, Python or curl.

    And for a Windows host with Node.js 24.22.0 from the official installer, Python 3.12 on Brotli 1.2.0 and .NET 8.0.21:

    # CVE-2026-111497 (Brotli decoder RCE): fixes for this Windows host, from @swamp/cve/http2shell.
    # Run in an elevated PowerShell session.
    winget upgrade --id OpenJS.NodeJS.LTS --exact --accept-source-agreements --accept-package-agreements
    & 'C:\Python312\python.exe' -I -m pip install --upgrade 'Brotli>=1.2.1' 'brotlicffi>=1.2.1.0'
    # C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.21: .NET 8.0.21 bundles Brotli 1.1.0 and has no fix yet; install the fixed runtime when Microsoft releases it.
    # Then restart the apps and services that use Node.js or Python.

Find every decoder before someone else does

Pull the scanner and run it against every host, container and image you own.

swamp extension pull @swamp/cve/http2shell