HTTP2Shell (CVE-2026-111497): Is Your Node or Python App Exposed?
A Brotli decoder bug hits Node.js, Bun, Python and more. Upgrade Node to 22.23.4, 24.22.1 or 26.12.1 and pip Brotli to 1.2.1, then check what your app fetches.
HTTP2Shell, also called Node2Shell or Broccoli2Shell, is CVE-2026-111497: an out-of-bounds write in Google’s Brotli decompression library. According to the CVE record, a crafted Brotli stream can corrupt memory, leak memory contents, crash the process or “potentially execute arbitrary code”. Despite the name, the bug has nothing to do with HTTP/2. Your app is exposed wherever it decompresses Brotli data that someone else controls, and the most common way that happens isn’t through uploads. It happens when your server fetches a URL and the HTTP client decompresses the response for you. The fix is Node.js 22.23.4, 24.22.1 or 26.12.1 (released October 21), Brotli 1.2.1 or brotlicffi 1.2.1.0 from pip (GitHub advisory), and your distro’s patched brotli package for everything that links the system library.
Key takeaways
- There are three separate fixes. Node.js and Bun bundle their own Brotli, so you fix them by upgrading the runtime. In Python the decoder comes from a pip package, so upgrading Python does nothing. curl, git and distro-packaged Node use the operating system’s library, so you fix those with
aptordnf. - Outbound HTTP is the big exposure. Node’s built-in
fetchdecodesbrresponses throughnode:zlib. So does axios. In Python, urllib3, httpx and aiohttp do the same whenever Brotli is installed. Webhooks, link previews, scrapers, image proxies and AI agents that browse the web all decode responses from servers you don’t control. - Express 5 decompresses Brotli request bodies by default. Express 4 doesn’t. body-parser 2.x inflates
brbodies unless you setinflate: false. - Bumping axios or undici won’t fix anything. They call Node’s own zlib, and the vulnerable code lives in the Node binary.
- No exploitation in the wild has been reported. But Imperva ran its own code on an official Node.js 24.22.0 image, so treat this as a real remote code execution bug.
- Node.js 20 and older get no fix. Node says end-of-life lines are always affected when it ships a security release.
What we know as of October 27, 2026
This post reflects sources we checked up to about 09:20 UTC on October 27. We’ll update it as things change.
Confirmed by the vendors. Google, the CVE numbering authority, lists Brotli 1.0.3 up to 1.0.10, 1.1.0, and 1.2.0 as affected. The fixed releases v1.0.10, v1.1.1 and v1.2.1 shipped on October 21. Node.js published its security release the same day and describes the root cause in one line: an invalid input was treated as “need more data” rather than as an error, so the decoder kept writing into a fixed eight-byte buffer. The CVE record was published on October 22. The finders, per Node.js, were Khai Tran and Sylvie Mayer of OpenAI Codex Security.
The names. Node.js doesn’t use a nickname. Imperva calls it Node2Shell, and so does GitHub’s advisory title. Other write-ups use HTTP2Shell, and an unaffiliated tracker uses Broccoli2Shell. All three names refer to the same CVE.
The severity scores disagree. Google scores it 9.2 Critical under CVSS v4.0, and GitHub rates it critical. Node.js rates it high. Red Hat says Important, 8.1 under CVSS v3.1, and Microsoft gives the same 8.1 for .NET. Ubuntu gives it medium priority, which one oss-security poster publicly questioned. Our read: the score matters less than whether your code decodes untrusted Brotli. If it does, patch now.
Exploitation status.
- Imperva’s red team says it ran code on the official Node.js 24.22.0 Linux arm64 image, using both a response read by
fetch()and an Express 5 request body. Imperva says it took several attempts, it didn’t test other builds, and its exploit isn’t public. The patched 24.22.1 rejected the same input. - Broccoli2Shell.com, the unaffiliated tracker, says a public proof of concept appeared on October 22 that only gets code execution on a deliberately weakened build. We haven’t verified that.
- CISA’s assessment on the CVE record was “Exploitation: none” as of October 23. The CVE wasn’t in the Known Exploited Vulnerabilities feed (catalog version 2026.10.26) when we checked this morning.
Where untrusted Brotli actually gets decoded
You don’t have to call zlib yourself to be exposed. There are three common paths.
1. Outbound HTTP clients. This is the one teams miss. When your server requests a URL, the remote server picks the Content-Encoding, and many clients decode it automatically:
- Node’s built-in
fetch(undici) advertisesbrand pipesbrresponses throughzlib.createBrotliDecompress. Imperva’s proof used exactly this path. - axios in Node advertises and decodes
br, also throughnode:zlib. - In Python, urllib3, which sits under
requests, addsbrto Accept-Encoding and decodes it wheneverbrotliorbrotlicffiis importable. httpx and the aiohttp client work the same way.
Any feature that fetches a URL a user or a third party can influence counts: webhook delivery, link previews, Open Graph image fetchers, RSS readers, scrapers, image proxies, “import from URL”, and AI agents with a browse or fetch tool. Vercel’s own bulletin makes the point directly: its WAF rules cover inbound traffic, but not “responses your functions fetch from other servers”. Only the runtime patch covers those.
2. Servers that decompress request bodies.
- Express 5 / body-parser 2.x:
json(),raw(),text()andurlencoded()inflategzip,br,deflateandzstdby default. Brotli support arrived in body-parser 2.0.0. The body-parser 1.x used by Express 4 inflates only gzip and deflate. - Fastify: handles compressed request bodies only if you register
@fastify/compress, which then decompresses every listed encoding, includingbrunless you restrictrequestEncodings. - aiohttp server:
auto_decompressis True by default, so request bodies are decoded whenever Brotli is installed. - A bare Node
httpserver doesn’t decode Brotli bodies just by receiving them, per Imperva.
We didn’t find documentation saying Next.js, FastAPI or Django decompress request bodies by default, and we haven’t tested them. Use the check below rather than trusting our silence.
3. Your own code. Any zlib.brotliDecompress, createBrotliDecompress, or brotli.decompress() call in Python that runs on uploaded files, queue messages or cached blobs from elsewhere. Node’s advisory covers any app that decompresses attacker-supplied data with node:zlib.
For comparison, Microsoft says that in .NET, ASP.NET Core and HttpClient do not decompress Brotli by default, so you’re exposed only if you turned it on. Node and Python have no such safe default.
Fixed versions, and what kind of job each one is
| Component | Fixed in | How you get the fix |
|---|---|---|
| Node.js (nodejs.org builds, nvm, official Docker images) | 22.23.4, 24.22.1, 26.12.1 | Runtime upgrade, then restart or redeploy. The official images were rebuilt on October 21 |
| Node.js from Ubuntu or Debian packages | Ubuntu marks its Node packages not affected because they use the system brotli (except 20.04, still “needed”). Debian notes the same | Update the system brotli package, then restart Node |
| Bun | 1.4.4 (moves to Brotli 1.2.1) | bun upgrade, or rebuild images |
Python Brotli (pip) |
1.2.1 | Package update. There’s no 1.1.x fix on PyPI, so go to 1.2.1 |
Python brotlicffi (pip) |
1.2.1.0 | Package update |
| System library: Ubuntu | USN-8956-1: 1.0.9-2ubuntu0.1 (22.04), 1.1.0-2ubuntu0.1 (24.04), 1.2.0-3ubuntu0.1 (26.04) |
apt upgrade, then restart anything linked to it |
| System library: Debian | 1.0.9-2+deb12u1 (bookworm), 1.1.0-2+deb13u1 (trixie) |
apt upgrade |
| System library: Alpine | 1.2.1-r0 on 3.23, 3.24 and edge |
apk upgrade |
| System library: RHEL 8, 9, 10 | Listed as affected, no fix released yet | Wait for Red Hat |
| Envoy (with the Brotli decompressor configured) | 1.40.0, 1.39.3, 1.38.6, 1.37.8, 1.36.12 | Proxy upgrade |
| .NET | 8.0.33, 9.0.22, 10.0.14 | Runtime update. Self-contained and NativeAOT apps must be rebuilt and redeployed |
Go, github.com/andybalholm/brotli |
v1.2.7 ports the upstream fix (October 23) | go get and rebuild |
| Git for Windows | 2.56.0.windows.4 | Installer update |
| PHP Windows builds | 8.5.12-1, 8.4.27-1, 8.3.36-1, 8.2.35-1 | New Windows package. Only reachable through cURL with automatic decompression on |
PHP brotli extension |
0.21.1 | Extension update |
Alpine’s 3.22 branch had no fixed brotli entry when we checked. On Linux, curl and git’s HTTPS transport link the system library. One oss-security post points out that git-remote-https is exposed when libcurl is built with Brotli, which is the usual case on distros, so the apt or dnf update fixes both. The official curl for Windows build 8.23.0_5 is statically linked with Brotli 1.2.1. The Ruby brotli gem is listed as fixed in 0.9.1 by Broccoli2Shell.com, but we couldn’t confirm that from the maintainer.
How to check your setup
Node.js
# The version Node's advisory keys on. Fixed: 22.23.4, 24.22.1, 26.12.1 or later
node -v
# Inside every running container, not just your laptop
docker exec your-container node -v
# Did this Node come from your distro? If so, the system library matters too
dpkg -S "$(readlink -f "$(command -v node)")" 2>/dev/null
dpkg -l | grep -i libbrotli # Debian/Ubuntu
rpm -q brotli # RHEL family
apk info -v 2>/dev/null | grep -i brotli # Alpine
If dpkg -S names a Debian or Ubuntu nodejs package, you’re on the distro build, and the libbrotli update is the one that counts. If it prints nothing, the binary came from nodejs.org, nvm, a tarball or a Docker image, and you need the new Node release.
Also check the version string in your CI and Dockerfiles. A FROM node:24 tag picks up the fix on the next build only if the build doesn’t reuse a cached layer, and a digest-pinned image never will. Rebuild with --pull.
Python
# Is a Brotli decoder installed, which version, and what pulled it in?
pip show Brotli brotlicffi 2>/dev/null | grep -E '^(Name|Version|Required-by)'
# Fix: anything older than 1.2.1 / 1.2.1.0 is affected
pip install --upgrade "Brotli>=1.2.1"
pip install --upgrade "brotlicffi>=1.2.1.0"
# Which lockfiles mention it?
grep -il brotli requirements*.txt poetry.lock uv.lock Pipfile.lock 2>/dev/null
The Required-by line matters. Many teams never asked for Brotli: a dependency pulled it in, and from that moment requests, httpx and aiohttp started decoding br responses. If the package is installed in your image, assume your HTTP clients use it. Debian also ships the decoder as a distro package, and its python-brotlicffi uses the system library.
Does your API decompress br request bodies?
Send a harmless, valid compressed body to an endpoint in staging and see what comes back:
printf '{"ping":1}' | brotli -c > /tmp/ping.br # needs the brotli CLI
curl -sS -X POST https://staging.example.com/api/your-json-endpoint \
-H 'Content-Type: application/json' \
-H 'Content-Encoding: br' \
--data-binary @/tmp/ping.br \
-o /dev/null -w '%{http_code}\n'
If the endpoint behaves as it does for a normal JSON request, even with a validation error about the content, it decompresses Brotli bodies. A 415 usually means it refuses compressed bodies. Check every framework and service you run, not just the main API.
What does your server fetch?
# Rough inventory of outbound HTTP in a codebase
grep -rnE "fetch\(|axios|undici|got\(|requests\.(get|post)|httpx|aiohttp\.ClientSession" \
--include='*.js' --include='*.ts' --include='*.py' . | grep -v node_modules | head -50
Then sort the hits into two groups: calls whose URLs are fixed and trusted (your own APIs, Stripe), and calls whose URLs come from users, customers, feeds or model output. The second group decides how urgent this is for you.
If you can’t patch today
For Node and Python the fix is a version bump, so the honest advice is to do it today. The documented interim measures are narrow:
- Express 5: body-parser’s
inflate: falserejects compressed request bodies, gzip included, so test it against real clients first. It does nothing for outboundfetch. - Fastify with
@fastify/compress: dropbrfromrequestEncodings. - Envoy: only deployments with the Brotli decompressor configured are affected.
- .NET: Microsoft documents removing
brfrom request decompression and fromAutomaticDecompression. It warns not to add decompression settings you weren’t already using. - Vercel: WAF rules for the inbound path went live on October 21. Vercel says Node 22.x and 24.x projects need no version change, but you should redeploy to activate runtime protections, and Node 20.x projects must move to 22.x or 24.x. We didn’t find comparable statements from AWS Lambda, Netlify or Cloudflare when we checked, so look at your provider’s status page.
- Imperva suggests temporarily limiting untrusted Brotli traffic on affected paths, after testing against normal traffic.
A WAF can’t protect the outbound path. If a feature fetches attacker-chosen URLs and you can’t upgrade the runtime today, consider switching that one feature off until you can.
How to tell whether you were hit
There’s not much published guidance yet, and nobody has reported attacks. Imperva says unexpected process exits during decompression, and decoder errors, may help you prioritize, but they don’t prove exploitation by themselves. Correlate them with the runtime version and where the compressed data came from.
Node’s fix adds a new decoder error, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. That’s our own inference, not published guidance: after you patch, seeing that error in your logs tells you someone is sending streams the old decoder mishandled. That makes it worth tracing back to its source, but it isn’t proof of compromise.
Beyond that, general incident hygiene applies. None of this is specific to this CVE:
# Crash and restart loops in Node or Python services (systemd)
journalctl -u your-service --since "2026-10-21" | grep -iE "segfault|SIGSEGV|core dumped|brotli"
# Kernel-level segfault records
dmesg -T | grep -i segfault
# Container restarts in Kubernetes
kubectl get pods -A --sort-by='.status.containerStatuses[0].restartCount' | tail -20
The CVE lists memory disclosure as a possible impact. If a service shows signs of compromise, rebuild it from a known-good image, and rotate every secret that process could read.
The bigger lesson
Last week’s PHP2Shell was hard because the fix was in the runtime, not the app. This one is harder because the same C library sits in at least four kinds of places in a typical stack: inside the Node or Bun binary, inside a pip wheel, in the operating system’s shared library, and statically compiled into proxies, Git for Windows and .NET apps. Each has a different owner and a different update path. Running npm update covers none of them.
Most teams couldn’t tell you this morning whether Brotli is in their Python image, or which of their features fetch URLs a user typed. An SBOM per service and an inventory of outbound HTTP calls turn that into a five-minute grep. Our technical due diligence post shows how to generate the SBOM, and our production-readiness checklist covers the rest of knowing what you run. AI-built Node and Next.js apps tend to have both problems: fetch-anything features like link previews and agent tools, and nobody who knows which Node version is deployed.
Also, Node 20 is out of support. If this CVE is what tells you that, it isn’t the last one that will.
If nobody owns this for you
If you read this and couldn’t say which Node version your production runs, or whether Brotli is in your Python image, that’s the gap our Senior Team Retainer fills. Senior engineers handle patching, runtime upgrades, dependency inventory, and moving you off end-of-life versions before they turn into incidents. It costs $6,000 a month for 30 senior hours or $9,500 for 50. You can cancel anytime, and if the first week doesn’t feel worth it, we refund the first month.
To be clear about the limits: it isn’t an on-call or 24/7 service. We work business hours on CET, which overlaps US mornings. If you need on-call cover, tell us and we’ll quote it separately. If your app was built with Lovable, Bolt, Cursor or similar and you’re not sure what it fetches or which runtime it runs on, our 72-hour AI-App Rescue Audit is the faster first step.