# Node2Shell Isn't Just Node: Patch the Brotli RCE (CVE-2026-111497) in Python, Django and Every Runtime You Ship


On October 27, 2026, CISA added CVE-2026-111497 to its Known Exploited Vulnerabilities catalog. The bug has collected three nicknames in a week (Node2Shell, HTTP2Shell and Broccoli2Shell), and most of the coverage frames it as a Node.js problem. It isn't, really. The flaw lives in Google's Brotli C decoder, and that decoder is vendored into Node.js, Bun, .NET, Envoy, Istio, Java's brotli4j, Linux distro packages, and the two Python packages that `requests`, `urllib3`, `httpx` and `aiohttp` quietly use to decode `Content-Encoding: br` responses.

If you run a Django or FastAPI backend with Brotli support installed, a Next.js front end, a .NET API, or a service mesh, this post is the checklist we'd want in front of us today: what is actually affected, who has to act, the fixed versions, commands to find vulnerable installs, and the workarounds that work (and one that doesn't).

We've kept facts and opinion separate. Everything in the "What we know" sections comes from the primary advisories linked inline. The "Our take" sections are our opinion, labelled as such.

## TL;DR

- **The bug:** an out-of-bounds write in the Brotli decoder (`c/dec/decode.c`) in Google Brotli **1.0.3 through 1.2.0**. A crafted Brotli stream can crash the process, leak memory, or potentially lead to remote code execution, with no authentication needed. ([Canadian Centre for Cyber Security AL26-027](https://www.cyber.gc.ca/en/alerts-advisories/al26-027-critical-vulnerability-impacting-google-brotli-cve-2026-111497), [Ubuntu CVE page](https://ubuntu.com/security/CVE-2026-111497))
- **Severity:** CVSS 4.0 score 9.2 (Critical) in the [GitHub advisory GHSA-9v8h-qqgm-4m35](https://osv.dev/vulnerability/GHSA-9v8h-qqgm-4m35). Node.js itself rates it High.
- **Exploitation:** CISA added it to KEV on October 27 and gave US federal agencies until November 10 to patch. Honeypot operators Previdian and ELLIO reported exploitation attempts on October 26 and 27 ([SecurityWeek](https://www.securityweek.com/cisa-warns-of-node2shell-exploitation-as-honeypots-log-attack-attempts/), [BleepingComputer](https://www.bleepingcomputer.com/news/security/cisa-warns-of-hackers-now-exploiting-critical-node2shell-flaw/)).
- **Fixed versions:** Brotli 1.0.10 / 1.1.1 / 1.2.1; Node.js 22.23.4 / 24.22.1 / 26.12.1; PyPI `Brotli` 1.2.1 and `brotlicffi` 1.2.1.0; .NET 8.0.33 / 9.0.22 / 10.0.14.
- **Who must act:** anyone whose code, or a library it uses, decompresses Brotli data from a source they don't control. That includes outbound HTTP clients, not only servers.

## What we know: the vulnerability

Brotli is the compression format behind `Content-Encoding: br`, which most browsers and CDNs use for HTML, CSS and JavaScript. A Brotli stream can signal a "block switch", which tells the decoder to change which Huffman tree it uses for literals, commands or distances.

According to the [Node.js October 21 security release](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases), "an invalid block switch was reported as a temporary need for more input instead of a permanent format error, so the streaming decoder kept appending input bytes to an eight-byte internal buffer." That is the out-of-bounds write. The [CVE record lists the weakness](https://osv.dev/vulnerability/GHSA-9v8h-qqgm-4m35) as CWE-252 (unchecked return value), and the Canadian Cyber Centre describes the result as CWE-787 (out-of-bounds write).

The upstream fix, Google Brotli commit `0ed4268`, makes the decoder return a dedicated error, `BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH`, and pass it back to the caller, so a malformed stream is rejected rather than treated as incomplete. Node.js cherry-picked that exact commit into its patched releases ([Node.js 26.12.1 release notes](https://nodejs.org/en/blog/release/v26.12.1)).

The Node.js advisory credits OpenAI Codex Security researchers with reporting the issue. Node.js and Google shipped fixes on October 21. Imperva and ELLIO have both said they achieved code execution against official Node.js builds on x86-64, per [SecurityWeek's report](https://www.securityweek.com/cisa-warns-of-node2shell-exploitation-as-honeypots-log-attack-attempts/).

### What "exploited" means right now

It's worth being precise here, because headlines blur it. CISA's KEV listing did not include details of the attacks. ELLIO said the in-the-wild attempt it captured used delivery components that matched public proof-of-concept code, but the final stage was broken and did not produce a shell in any of ELLIO's 50 test runs. ELLIO also warned that failed attempts can crash the target process, so even a broken exploit is a denial-of-service risk. Previdian recorded one attempt on October 27 and stressed that its "confirmed" label is about whether the traffic was really targeting this bug, not whether the attempt succeeded.

So the state of play, as reported: working exploits exist in researchers' hands, public PoC code is circulating, and mass-exploitation operators are already firing early versions at the internet.

## What we know: who is affected

The key question isn't "do I have Brotli installed?" It's "does any process I run decompress Brotli data that an attacker could influence?" Here's the affected list compiled from the vendor advisories and the [New York State ITS advisory 2026-120](https://its.ny.gov/2026-120).

| Component | Affected | Fixed |
| --- | --- | --- |
| Google Brotli (C library) | 1.0.3 to 1.0.9, 1.1.0, 1.2.0 | 1.0.10, 1.1.1, 1.2.1 |
| Node.js 22.x / 24.x / 26.x | before 22.23.4 / 24.22.1 / 26.12.1 | 22.23.4 / 24.22.1 / 26.12.1 |
| Node.js 20.x, 23.x, 25.x | end of life, affected | no fix; move to a supported line |
| Bun | 1.4.3 and earlier | upgrade (Akamai's detection guidance flags anything below 1.4.4) |
| PyPI `Brotli` | 1.2.0 and earlier | 1.2.1 (published October 22) |
| PyPI `brotlicffi` | 1.2.0.x and earlier | 1.2.1.0 (published October 22) |
| .NET 8 / 9 / 10 | before 8.0.33 / 9.0.22 / 10.0.14 | 8.0.33 / 9.0.22 / 10.0.14 |
| Envoy / Istio | when the Brotli decompressor is configured | Envoy 1.40.0, 1.39.3, 1.38.6, 1.37.8; Istio 1.29.9, 1.30.6, 1.31.2 |
| Java brotli4j | before 1.24.0 | 1.24.0 |
| VMware SDDC Manager | 9.1.1 (CVSS 9.8), 9.0.x, 9.1.0, 5.2.x | patches pending, workarounds published |

.NET Framework is not affected, per [Microsoft's advisory](https://github.com/dotnet/runtime/security/advisories/GHSA-p3jp-9v83-76w9). Palo Alto Networks is rolling out PAN-OS hotfixes, according to SecurityWeek.

### The attack paths, runtime by runtime

**Node.js.** [Akamai's analysis](https://www.akamai.com/blog/security-research/inside-node2shell-closer-look-cve-2026-111497) lists three ways attacker data reaches the decoder without your code ever calling a Brotli API:

1. `node:zlib`, if you pass untrusted input to `zlib.brotliDecompress()` or `zlib.createBrotliDecompress()`.
2. The built-in `fetch()`, which automatically decompresses Brotli responses. A malicious or compromised server can attack the client.
3. Express 5 and body-parser 2, whose `json`, `raw`, `text` and `urlencoded` parsers decompress request bodies sent with `Content-Encoding: br` by default, because `inflate` defaults to `true`. Any client can send that request.

A bare Node.js HTTP server does not decode incoming Brotli bodies on its own. Akamai also says its telemetry shows 61% of networks running affected Node.js versions.

**.NET.** Microsoft is explicit that exposure needs one of three things: ASP.NET Core request decompression with `br` enabled, `HttpClient` automatic decompression configured to include `DecompressionMethods.Brotli` or `DecompressionMethods.All`, or direct use of `BrotliStream` / `BrotliDecoder` on attacker-controlled data. None of the automatic paths are on by default.

**Python.** The PyPI `Brotli` package is Google's own CPython extension and compiles the same `decode.c`. `brotlicffi` (the CFFI binding used on PyPy) vendors the same source. [OpenSourceMalware's write-up](https://opensourcemalware.com/blog/node2shell-affects-python) diffed the `Brotli` 1.2.0 and 1.2.1 source distributions and found that the only changes besides the version bump are the `decode.c` and `decode.h` changes from the upstream fix commit. The PyPA advisory database now tracks them as PYSEC-2026-4322 (`brotli`) and PYSEC-2026-4323 (`brotlicffi`), and the GitHub advisory has been updated to include both.

## The Python and Django angle most coverage skipped

This is where we think a lot of backend teams have a blind spot, so we tested it ourselves on a clean virtualenv with `requests`, `urllib3` 2.8.0 and `Brotli` 1.2.0.

**Fact 1: installing Brotli changes what your HTTP client asks for.** With `Brotli` importable, `requests` advertises Brotli in every request:

```python
>>> import requests
>>> requests.utils.default_headers()["Accept-Encoding"]
'gzip, deflate, br'
```

**Fact 2: urllib3 decodes `br` responses even when you didn't ask for them.** We ran a tiny local server that always replies with `Content-Encoding: br`, then called it with an explicit `Accept-Encoding: gzip, deflate` header. `requests` still handed the response to the Brotli decoder and returned the decompressed text. urllib3's decoder list (`HTTPResponse.CONTENT_DECODERS`) contains `br` whenever a Brotli module is importable, and it picks the decoder based on the *response* header, not on what you requested.

Here's the reproduction so you can check your own stack (it uses a valid stream, so it is harmless):

```python
# server.py - replies with Brotli no matter what the client asked for
import http.server, brotli

class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        body = brotli.compress(b"hello from br")
        self.send_response(200)
        self.send_header("Content-Encoding", "br")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

http.server.HTTPServer(("127.0.0.1", 8765), H).serve_forever()
```

```python
# client.py
import requests
r = requests.get("http://127.0.0.1:8765",
                 headers={"Accept-Encoding": "gzip, deflate"})
print(r.content)   # b'hello from br'  -> the Brotli decoder ran anyway
```

That matters because the common advice "strip `br` from `Accept-Encoding` on outbound requests" reduces the chance that an honest server sends you Brotli, but **it does not protect you from a malicious server**, which is exactly the attacker in this scenario. If you need a workaround before you can upgrade, remove the module so urllib3 can't decode `br` at all.

**Fact 3: version pins won't save you.** `urllib3[brotli]` declares `brotli>=1.2.0`, `httpx[brotli]` pulls in `brotli` with no lower bound, and `aiohttp[speedups]` asks for `Brotli>=1.2`. Every one of those constraints is satisfied by the vulnerable 1.2.0. Your lockfile only becomes safe once it actually resolves to 1.2.1.

**Where Django projects pick up Brotli.** A common route is `whitenoise[brotli]`, which installs `brotli` so WhiteNoise can pre-compress static files into `.br` files at `collectstatic` time. WhiteNoise only *compresses*, and the bug is in the *decoder*, so WhiteNoise itself isn't the attack path. But once `brotli` is in the same environment, every `requests`, `urllib3` and `httpx` call in your Django app (webhook senders, payment provider clients, OAuth token fetches, link-preview and "import from URL" features, scrapers, LLM tool calls that fetch pages) will decode Brotli responses from whatever server answers. Celery workers that share the same image inherit it too.

### Python exposure checklist

Prioritise the services that fetch from servers you don't control:

- "Fetch this URL" features: link previews, RSS/feed import, avatar-from-URL, document import, web scrapers.
- AI agents and RAG pipelines with a browsing or fetch tool. An agent that follows links from model output or user input is fetching attacker-chosen URLs by design.
- Webhook *delivery* to customer-supplied endpoints. The customer controls the server that answers.
- Anything behind SSRF protections that still allows outbound HTTP to the public internet.
- Inbound: any middleware or framework you've configured to decode `Content-Encoding: br` request bodies.

## How to find vulnerable installs

### Python

```bash
# In every virtualenv, container image and CI runner
pip show Brotli brotlicffi 2>/dev/null | grep -E '^(Name|Version)'

# Or ask the import system directly (covers whichever module urllib3 would use)
python - <<'PY'
import importlib.metadata as md
for pkg in ("Brotli", "brotlicffi"):
    try:
        print(pkg, md.version(pkg))
    except md.PackageNotFoundError:
        print(pkg, "not installed")
PY

# Advisory-aware scan (pip-audit reads the PyPA advisory database)
pip install pip-audit && pip-audit
```

To scan a lockfile without installing anything:

```bash
grep -iE '^(brotli|brotlicffi)[=<>~ ]' requirements*.txt poetry.lock uv.lock 2>/dev/null
```

If `brotlipy` (the old, unmaintained binding that also installs a module called `brotli`) turns up in a legacy project, replace it with `Brotli>=1.2.1`. We did not find a patched `brotlipy` release.

### Node.js and Bun

```bash
node -v      # need >= 22.23.4, 24.22.1 or 26.12.1
bun -v       # upgrade if 1.4.3 or earlier

# Inside every running container
docker ps --format '{{.Names}}' | while read c; do
  printf '%s: ' "$c"; docker exec "$c" node -v 2>/dev/null || echo "no node"
done
```

Don't forget CI runners, Electron apps (which bundle their own Node), serverless runtimes you pin yourself, and base images like `node:22-alpine` that you built weeks ago and never rebuilt. A Dockerfile that says `FROM node:22` is only patched after you pull and rebuild.

### .NET

```bash
dotnet --list-runtimes   # need 8.0.33, 9.0.22 or 10.0.14 or later
```

Microsoft notes that self-contained apps must be rebuilt and redeployed, and NativeAOT apps must be republished with the patched SDK, because "updating the machine's shared runtime alone does not replace a decoder already linked into a NativeAOT executable" ([dotnet/announcements #494](https://github.com/dotnet/announcements/issues/494)).

### Linux packages

Distro `brotli` packages are being patched separately. Ubuntu, for example, lists fixed builds such as `1.1.0-2ubuntu0.1` on 24.04 LTS and `1.0.9-2ubuntu0.1` on 22.04 LTS ([Ubuntu CVE tracker](https://ubuntu.com/security/CVE-2026-111497)).

```bash
# Debian/Ubuntu
apt list --installed 2>/dev/null | grep -i brotli
sudo apt update && sudo apt install --only-upgrade libbrotli1 brotli

# RHEL/Fedora family
rpm -qa | grep -i brotli
sudo dnf upgrade brotli
```

Then restart anything that has the old shared library mapped. A patched `.so` on disk does nothing for a process that loaded the old one last week.

## How to fix it

**1. Upgrade, rebuild, redeploy.** This is the only complete fix. The Node.js advisory offers no workaround, as Akamai points out.

```bash
# Python
pip install -U "Brotli>=1.2.1" "brotlicffi>=1.2.1.0"
# then regenerate your lockfile, e.g.
uv lock --upgrade-package brotli     # or: poetry update brotli
```

```dockerfile
# Node: pin the patched patch release explicitly so a stale cache can't win
FROM node:22.23.4-bookworm-slim
```

```bash
# Rebuild without cache so the base image and OS packages are refreshed
docker build --pull --no-cache -t myapp:patched .
```

For .NET, install 8.0.33, 9.0.22 or 10.0.14 (or the matching SDKs listed in Microsoft's advisory), restart shared-runtime apps, and rebuild self-contained and NativeAOT apps.

If you're on Vercel, their [security bulletin](https://vercel.com/kb/bulletin/node-js-security-advisory) says all new deployments use a patched Node.js as of October 28, and projects still on Node.js 20.x must switch to 22.x or 24.x and redeploy. Vercel's WAF rules cover inbound traffic only, not responses your functions fetch from other servers, and they say plainly that it "does not replace patching".

**2. If you can't patch today, shrink the reachable surface.**

Express 5 / body-parser 2, if you don't need compressed request bodies:

```js
app.use(express.json({ inflate: false }));
app.use(express.urlencoded({ extended: false, inflate: false }));
// compressed bodies are now rejected with HTTP 415
```

Python, if you don't need Brotli right now:

```bash
pip uninstall -y Brotli brotlicffi
```

`urllib3`, `requests`, `httpx` and `aiohttp` fall back to gzip and deflate when no Brotli module is importable, and with no module present `br` drops out of urllib3's decoder list, so a hostile `Content-Encoding: br` response is left undecoded instead of being decoded. If you use WhiteNoise, it will simply stop generating `.br` files until you reinstall the patched package; gzip files are still served.

.NET: Microsoft's guidance is to avoid decompressing attacker-controlled Brotli data until you've updated, and to modify only *existing* decompression configuration. They specifically warn not to add `AddRequestDecompression` or `AutomaticDecompression` settings if you weren't already using them, because automatic decompression has its own denial-of-service risks.

Envoy and Istio: you're only exposed when the Brotli decompressor filter is configured. If it is and you can't upgrade, remove that filter for traffic from untrusted sources.

**3. Watch for crashes.** The Canadian Cyber Centre recommends monitoring for unexpected crashes and abnormal process behaviour. Given ELLIO's observation that broken exploit attempts crash the target, a burst of segfaults or restarts in Node, Python or .NET workers since October 25 is worth a look, especially on services that fetch external URLs or accept compressed bodies.

```bash
# Kubernetes: who has been restarting?
kubectl get pods -A --sort-by='.status.containerStatuses[0].restartCount' | tail -20

# systemd hosts: segfaults in the kernel log
journalctl -k --since "2026-10-25" | grep -i segfault
```

**4. Restrict egress for the riskiest workers.** Fetchers, crawlers and agent tool-runners are the processes most likely to talk to a hostile server. Akamai also recommends segmenting inbound access and limiting outbound connections from assets running vulnerable Node.js.

## Our take (opinion)

Everything above is sourced. This section is our view.

**Treat your HTTP clients as attack surface, not only your servers.** The headlines say "Node.js RCE", and the reflex is to check which internet-facing Node servers you run. But the more interesting path in 2026 is the outbound one. Modern apps fetch constantly: webhooks, enrichment APIs, OG-image previews, and now AI agents that browse on behalf of users. Every one of those is a decoder running on bytes chosen by whoever controls the other end. If your team has been building agents with fetch tools, this is a good week to make sure those tools run in a sandboxed worker with locked-down egress and a short-lived identity, not inside your main web process with database credentials in its environment.

**"Silent" fixes are a supply-chain problem.** The Python packages shipped their fixes on October 22 without mentioning the CVE in release notes, and it took outside researchers to get PyPA advisories filed. Scanners only know what advisory databases tell them. Our practical rule: when a CVE hits a C library, search your dependency tree for every binding of that library across every language you ship, rather than waiting for each ecosystem's scanner to light up.

**Rebuild, don't just bump.** A common gap after a dependency CVE is the image nobody rebuilt: the cron container, the one-off migration job, the CI runner image, the internal admin tool. If you have a "rebuild all images from fresh bases" pipeline, run it today. If you don't, this incident is a decent argument for building one.

**Don't over-rotate on the workarounds.** Turning off `inflate` in Express or uninstalling Brotli in Python are fine stopgaps. Stripping `Accept-Encoding: br` on its own is not, as our test above shows. And for .NET, Microsoft's warning is clear: don't start adding decompression settings you weren't using in an attempt to "configure your way out".

**Prioritise by reachability.** If you have to sequence the work, our order would be:

1. Internet-facing Express 5 / body-parser 2 apps (any client can send a `br` body).
2. Services that fetch attacker-influenced URLs: agents, previewers, crawlers, webhook senders.
3. Edge proxies with the Brotli decompressor enabled (Envoy, Istio).
4. Everything else that bundles Brotli, including CI runners and dev machines.

## Timeline

All dates are as reported in the linked sources.

- **October 21, 2026:** Google releases Brotli 1.2.1, 1.1.1 and 1.0.10; Node.js ships 22.23.4, 24.22.1 and 26.12.1.
- **October 22, 2026:** CVE published in NVD; PyPI `Brotli` 1.2.1 and `brotlicffi` 1.2.1.0 released.
- **October 23, 2026:** Singapore's CSA issues an alert rating it CVSS 9.8; OpenSourceMalware publishes the Python analysis.
- **October 25, 2026:** Microsoft ships out-of-band .NET 8, 9 and 10 updates.
- **October 26, 2026:** ELLIO's sensors catch an exploitation attempt against Node.js 24.22.0; NY ITS and the Canadian Cyber Centre publish advisories.
- **October 27, 2026:** CISA adds CVE-2026-111497 to KEV (federal deadline November 10); Previdian logs an attempt; Broadcom flags VMware SDDC Manager as affected.
- **October 28, 2026:** Vercel moves all new deployments to patched Node.js.

## Primary sources

- [Node.js October 21, 2026 security releases](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases)
- [Google Brotli releases (1.2.1, 1.1.1, 1.0.10)](https://github.com/google/brotli/releases)
- [GitHub advisory GHSA-9v8h-qqgm-4m35 via OSV](https://osv.dev/vulnerability/GHSA-9v8h-qqgm-4m35)
- [Microsoft .NET advisory GHSA-p3jp-9v83-76w9](https://github.com/dotnet/runtime/security/advisories/GHSA-p3jp-9v83-76w9)
- [Canadian Centre for Cyber Security AL26-027](https://www.cyber.gc.ca/en/alerts-advisories/al26-027-critical-vulnerability-impacting-google-brotli-cve-2026-111497)
- [Ubuntu CVE-2026-111497 tracker](https://ubuntu.com/security/CVE-2026-111497)
- [Akamai: Inside Node2Shell](https://www.akamai.com/blog/security-research/inside-node2shell-closer-look-cve-2026-111497)
- [OpenSourceMalware: Python packages vulnerable to Node2Shell](https://opensourcemalware.com/blog/node2shell-affects-python)
- [SecurityWeek on KEV listing and honeypot data](https://www.securityweek.com/cisa-warns-of-node2shell-exploitation-as-honeypots-log-attack-attempts/)
- [Vercel security bulletin](https://vercel.com/kb/bulletin/node-js-security-advisory)

If you've found Brotli somewhere unexpected in your stack (a vendored binary, an appliance, a language binding we didn't list), tell us in the comments. Mapping where a shared C library actually ends up is the hardest part of incidents like this, and a shared list helps everyone patch faster.
