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, Ubuntu CVE page) - Severity: CVSS 4.0 score 9.2 (Critical) in the GitHub advisory 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, BleepingComputer).
- Fixed versions: Brotli 1.0.10 / 1.1.1 / 1.2.1; Node.js 22.23.4 / 24.22.1 / 26.12.1; PyPI
Brotli1.2.1 andbrotlicffi1.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, "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 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).
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.
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.
| 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. Palo Alto Networks is rolling out PAN-OS hotfixes, according to SecurityWeek.
The attack paths, runtime by runtime
Node.js. Akamai's analysis lists three ways attacker data reaches the decoder without your code ever calling a Brotli API:
node:zlib, if you pass untrusted input tozlib.brotliDecompress()orzlib.createBrotliDecompress().- The built-in
fetch(), which automatically decompresses Brotli responses. A malicious or compromised server can attack the client. - Express 5 and body-parser 2, whose
json,raw,textandurlencodedparsers decompress request bodies sent withContent-Encoding: brby default, becauseinflatedefaults totrue. 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 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:
>>> 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):
# 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()
# 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: brrequest bodies.
How to find vulnerable installs
Python
# 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:
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
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
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).
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).
# 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.
# 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
# Node: pin the patched patch release explicitly so a stale cache can't win
FROM node:22.23.4-bookworm-slim
# 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 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:
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:
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.
# 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:
- Internet-facing Express 5 / body-parser 2 apps (any client can send a
brbody). - Services that fetch attacker-influenced URLs: agents, previewers, crawlers, webhook senders.
- Edge proxies with the Brotli decompressor enabled (Envoy, Istio).
- 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
Brotli1.2.1 andbrotlicffi1.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
- Google Brotli releases (1.2.1, 1.1.1, 1.0.10)
- GitHub advisory GHSA-9v8h-qqgm-4m35 via OSV
- Microsoft .NET advisory GHSA-p3jp-9v83-76w9
- Canadian Centre for Cyber Security AL26-027
- Ubuntu CVE-2026-111497 tracker
- Akamai: Inside Node2Shell
- OpenSourceMalware: Python packages vulnerable to Node2Shell
- SecurityWeek on KEV listing and honeypot data
- Vercel security bulletin
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.
About the author
- Custom software & AI development company. We build production-ready AI integrations, mobile apps, and web platforms for businesses that want real results. lycore.com
Comments
Join the discussionNo comments yet. Be the first to comment.



