HTTP2Shell is the name now used for CVE-2026-111497 once it spread past Node.js. The bug is an out-of-bounds write in Google's Brotli decoder, versions 1.0.3 to 1.2.0, so it reaches Python's Brotli and brotlicffi packages, Envoy, Istio, Bun, libcurl builds and OS packages. Upgrade to Brotli 1.2.1, 1.1.1 or 1.0.10, or to each project's patched release.
When Node.js shipped its fix on 21 October, coverage called the flaw "Node2Shell" and treated it as a Node problem. We covered that side in our Node.js Brotli patching guide and do not repeat it here. The broken decoder is Google's C library, which sits inside far more than Node. By 23 October a security vendor's report had renamed the issue HTTP2Shell, because anything that decodes Content-Encoding: br traffic can carry it. This post covers everything that is not Node: the Python HTTP stack, Envoy and Istio, Bun, curl, and the libbrotli copies in your OS packages and images.
It cuts both ways. A malicious client can attack a server that decodes Brotli request bodies, and a malicious server can attack a client that decodes Brotli responses. Most teams only think about the first.
What happened
The fix sat in Brotli's source tree for about eight months before it shipped as a security release. That gap explains most of this week's confusion.
| Date and time (UTC, 2026) | Event | Source |
|---|---|---|
| 27 Feb, 13:10:32 | Brotli maintainer commits 0ed4268a, titled "Detect and report invalid stream earlier", touching c/dec/decode.c and c/include/brotli/decode.h | GitHub commit API, google/brotli |
| 27 Feb | The fix commit 0ed4268a lands in the Brotli repository; no release carries it until October | Brotli repository commit history |
| 6 Oct and 13 Oct | Envoy publishes v1.37.8, v1.38.6 and v1.39.3 (6 Oct, 14:43 to 18:22) and v1.40.0 (13 Oct, 20:02), the releases its later advisory lists as fixed | GitHub releases API, envoyproxy/envoy |
| 15 Oct, 23:58 to 16 Oct, 00:13 | Istio publishes 1.29.9, 1.30.6 and 1.31.2 with bulletin ISTIO-SECURITY-2026-007, which lists CVE-2026-111497 among "Envoy CVEs" | Istio bulletin and GitHub releases API |
| 16 Oct, 09:58:17 | Google, as CVE Numbering Authority, reserves CVE-2026-111497 | CVE record metadata |
| 21 Oct, about 10:38 | Node.js ships 22.23.4, 24.22.1 and 26.12.1, naming the CVE ("Node2Shell" in coverage) | Node.js October 2026 security release post |
| 21 Oct, 19:26:58 to 19:27:34 | Google tags Brotli v1.0.10, v1.1.1 and v1.2.1, each with one security line naming CVE-2026-111497 | GitHub releases API, google/brotli |
| 22 Oct, 10:11:32 | Bun v1.4.4 published with Brotli 1.2.1 | GitHub releases API, oven-sh/bun |
| 22 Oct, 13:35:41 | CVE record published with a CVSS 4.0 score of 9.2, Critical | CVE record (cve.org) |
| 22 Oct, 13:57:11 and 16:19:00 | PyPI Brotli 1.2.1 and brotlicffi 1.2.1.0 uploaded | PyPI JSON API |
| 22 Oct, 15:32:49 | GitHub advisory GHSA-9v8h-qqgm-4m35 published for PyPI brotli and brotlicffi, without a CVE ID attached | GitHub advisory API |
| 23 Oct | PyPA advisory database publishes PYSEC-2026-4322 (Brotli) and PYSEC-2026-4323 (brotlicffi), both aliased to the CVE | pypa/advisory-database |
| 23 Oct, 09:31:05 | Bun publishes GHSA-663w-cv8g-jj2p | GitHub advisory API, oven-sh/bun |
| 23 Oct, 16:03:27 | Envoy publishes GHSA-3wvj-j6qm-62mr, crediting OpenAI Security Research | GitHub advisory API, envoyproxy/envoy |
| 23 Oct | A security vendor's report renames the issue HTTP2Shell and widens the scope to "libraries and apps using HTTP" | Security vendor report |
| 23 Oct, 19:30 | CISA's KEV catalog refresh does not include the CVE | CISA KEV JSON feed |
| 24 Oct, 02:49 | OSV's record for the CVE now lists PYSEC-2026-4322 and PYSEC-2026-4323 as aliases | OSV API |
Two things stand out. Envoy and Istio shipped fixed builds days before the public Brotli release, so some meshes were patched before most people had heard of the bug. And Python's fix arrived as a quiet version bump on PyPI, which for about a day the advisory data that scanners read did not tie to the CVE.
Who is affected
| You run | Affected? | Fixed version | Source |
|---|---|---|---|
| libbrotli 1.0.3 up to but not including 1.0.10 | Yes | 1.0.10 | CVE record; Brotli releases |
| libbrotli 1.1.0 | Yes | 1.1.1 | CVE record |
| libbrotli 1.2.0 | Yes | 1.2.1 | CVE record |
PyPI Brotli 1.0.3 to 1.2.0 | Yes | 1.2.1 | GHSA-9v8h-qqgm-4m35 |
PyPI brotlicffi 1.0.3 to 1.2.0.2 | Yes | 1.2.1.0 | GHSA-9v8h-qqgm-4m35 |
| Bun 1.0.21 to 1.4.3 | Yes | 1.4.4 | GHSA-663w-cv8g-jj2p |
| Envoy v1.39.2, v1.38.5, v1.37.7, v1.36.11 with a Brotli decompressor configured | Yes | v1.40.0, v1.39.3, v1.38.6, v1.37.8 (and v1.36.12) | GHSA-3wvj-j6qm-62mr |
| Istio 1.31.0 to 1.31.1, 1.30.0 to 1.30.5, 1.29.0 to 1.29.8, if Brotli decompression is enabled | Yes | 1.31.2, 1.30.6, 1.29.9 | ISTIO-SECURITY-2026-007 |
| Node.js 22.x, 24.x, 26.x before the October release | Yes | 22.23.4, 24.22.1, 26.12.1 | Node.js advisory; see our Node post |
| curl or libcurl linked against an affected libbrotli | Yes, when decoding Brotli | Rebuild against a fixed libbrotli or take your vendor's package | Security vendor report |
Debian 12 brotli 1.0.9-2 | Yes | 1.0.9-2+deb12u1 (DLA-4855-1) | Debian security tracker |
Debian 13 brotli 1.1.0-2 | Yes | 1.1.0-2+deb13u1 (DSA-6582-1) | Debian security tracker |
Debian forky and sid brotli 1.2.0-4 | Yes | Unfixed as of 24 Oct (bug 1151690) | Debian security tracker |
Ubuntu brotli and nodejs, all listed releases | Needs evaluation per Ubuntu on 23 Oct | Watch for a USN | Ubuntu CVE tracker |
Apache httpd mod_brotli (standard) | Not affected per a security vendor's report, because it compresses and does not decode | n/a | Security vendor report |
| nginx default build | Not affected per the same report, because it does not decode br by default | n/a | Security vendor report |
Envoy's advisory names one version per branch ("v1.39.2, v1.38.5, v1.37.7 and v1.36.11"). We read those as the last vulnerable release in each branch, so treat anything older than your branch's fixed release as affected. A security vendor's report notes the 1.36 branch is unsupported, so v1.36.12 is a stopgap, not a destination.
How it works, in plain language
Brotli compresses data in blocks. A compressed stream can tell the decoder to switch between block types, which changes which Huffman table the decoder uses for the next stretch of data. A function called DecodeBlockTypeAndLength() handles those switches.
In vulnerable versions that function returned a plain yes or no, and "no" could mean either "this stream is malformed" or "I need more input." Callers could not tell those apart, and the fast decode path ignored the return value. The PyPA advisory entries describe the weakness pair as an unchecked return value (CWE-252) leading to an out-of-bounds write (CWE-787).
Node.js describes the consequence in its advisory. 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 internal buffer that Node says is eight bytes long. Bytes written past the end of that buffer corrupt adjacent decoder state in memory. The CVE description lists the possible outcomes: corrupted decoder state, disclosure of memory, denial of service, or "potentially execute arbitrary code."
The fix changes the function to return a real status. It can now say success, need more input, or a new error, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, which takes the error code -17 that the header previously marked as reserved. Every caller now checks the status and stops on a format error, on both the safe and fast paths, so a fixed decoder simply rejects the input that used to cause the overflow. We do not describe how to build a triggering stream; you do not need one to fix this.
Why the name changed
The first public research focused on Node.js, whose built-in fetch() decodes Brotli responses and whose popular body parsers decode Brotli request bodies. Once the PyPA, Bun, Envoy and Istio advisories showed the same C code elsewhere, the name moved to where the decoder actually sits: the HTTP layer, wherever a client or server honours Content-Encoding: br.
Both directions matter
| Direction | Who sends the Brotli data | Typical exposed code |
|---|---|---|
| Inbound | Any HTTP client that can reach you | Servers, proxies or middleware that decompress request bodies sent with Content-Encoding: br; an Envoy or Istio gateway with a Brotli decompressor filter on requests |
| Outbound | Any server your code connects to | HTTP clients that advertise br and decode responses: Python requests and urllib3 with Brotli installed, httpx with its Brotli extra, aiohttp, Scrapy, Bun's fetch, curl with compressed transfers enabled; Envoy decompressing upstream responses |
| Local | Files or messages your code reads | Code that calls a Brotli library directly on uploaded files, queue messages, cached objects or archives |
The outbound direction is the one teams miss. A web scraper, a link preview service, a webhook delivery checker or an AI agent that fetches URLs all talk to servers chosen by someone else. If that someone is hostile, the response is the attack.
Technical details
Every value below was read in the source named next to it. We publish no proof of concept and no triggering input.
Identifiers and scores
| Field | Value | Source |
|---|---|---|
| CVE | CVE-2026-111497 | CVE record |
| CNA | CVE record metadata | |
| Title | "Out-of-Bounds Write in Google Brotli Decoder" | CVE record |
| Reserved | 2026-10-16 09:58:17 UTC | CVE record metadata |
| Published | 2026-10-22 13:35:41 UTC | CVE record metadata |
| CVSS 4.0 vector | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N | CVE record; also GHSA-9v8h-qqgm-4m35 and GHSA-3wvj-j6qm-62mr |
| CVSS 4.0 score | 9.2, Critical | CVE record |
| CWE | CWE-252 (Unchecked Return Value), CWE-787 (Out-of-bounds Write) | OSV record; Envoy advisory |
| Credited finders | Two researchers at OpenAI, per the CVE record's credits; Envoy credits "OpenAI Security Research" | CVE record; Envoy advisory |
| CISA SSVC (23 Oct) | Exploitation: none; Automatable: no; Technical Impact: total | CISA ADP container in the CVE record |
| CISA KEV | Not listed as of the 23 Oct 19:30 UTC catalog | CISA KEV JSON feed |
| Other ratings | Node.js and Bun: High; GitHub (PyPI packages): Critical; Ubuntu priority: Medium | Respective advisories |
The ratings disagree. The CNA score is Critical, Node and Bun say High, and Ubuntu's priority is Medium. The CVSS 4.0 vector itself marks attack requirements as present (AT:P), which reflects that the target has to decode attacker-supplied Brotli data. Use the CNA score for consistency across tools, and use reachability to decide order.
Advisory identifiers
| Identifier | Covers | Source |
|---|---|---|
GHSA-9v8h-qqgm-4m35 | PyPI brotli and brotlicffi; updated 2026-10-23 19:08:11 UTC, still with an empty CVE field | GitHub advisory API |
PYSEC-2026-4322, PYSEC-2026-4323 | PyPI brotli and brotlicffi, both aliased to the CVE | PyPA advisory database |
GHSA-663w-cv8g-jj2p | Bun (npm package bun, 1.0.21 up to 1.4.4) | GitHub advisory API |
GHSA-3wvj-j6qm-62mr | Envoy | GitHub advisory API |
ISTIO-SECURITY-2026-007 | Istio, alongside other Envoy and Istio fixes | Istio security bulletins |
DSA-6582-1, DLA-4855-1, bug 1151690 | Debian trixie, bookworm and unstable | Debian security tracker |
Fix commits per Brotli branch
| Branch | Fixed release | Fix commit | Source |
|---|---|---|---|
| main | (source) | 0ed4268a4401abcb4472096340477cf4af7d1a2b | GitHub commit API |
| 1.2.x | v1.2.1, tagged 2026-10-21 19:27:34 UTC | b418fa81c48262a86a95332b0bc6d62c30aa78b6 | Debian tracker notes; GHSA references |
| 1.1.x | v1.1.1, tagged 2026-10-21 19:27:20 UTC | da9806362874b9bd4b552a3af360b5d6347beb12 | Debian tracker notes; GHSA references |
| 1.0.x | v1.0.10, tagged 2026-10-21 19:26:58 UTC | 50a9b70a4d5e1443fc40927952276b68fffa83a0 | Debian tracker notes; GHSA references |
The main commit 0ed4268a was authored by Evgenii Kliuchnikov on 2026-02-27 13:10:32 UTC. It adds 58 lines and removes 29 in c/dec/decode.c, and in c/include/brotli/decode.h it replaces the reserved -17 slot with BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. The release notes for each tag say only "CVE-2026-111497: out-of-bounds write permits remote code execution."
After you upgrade, decode errors with code -17 mean someone sent a stream that older versions would have mishandled; Bun's advisory confirms invalid data "now fails with the new decoder error." That is worth alerting on, though no source lists it as an indicator of compromise.
Python package release facts
| Package | Last affected | Fixed | Fixed upload time (UTC) | Notes | Source |
|---|---|---|---|---|---|
Brotli | 1.2.0 (uploaded 2025-11-05) | 1.2.1 | 2026-10-22 13:57:11 | PYSEC-2026-4322 says 1.2.1 "differs from 1.2.0 only in that fix plus a version bump" | PyPI JSON API; PyPA |
brotlicffi | 1.2.0.2 (uploaded 2026-08-21) | 1.2.1.0 | 2026-10-22 16:19:00 | Affected versions listed: 0.8.0, 1.0.9.0 to 1.0.9.2, 1.1.0.0, 1.2.0.0 to 1.2.0.2 | PyPI JSON API; PyPA |
One public write-up gives 23 October as the brotlicffi publication date; PyPI's own upload timestamp says 22 October at 16:19 UTC. We use PyPI's value.
Bun specifics
Bun's advisory is unusually precise about where the decoder is reached. Bun 1.4.3 bundles Brotli 1.2.0, and three APIs feed the decoder:
| Bun API | How Brotli reaches it |
|---|---|
fetch | Sends Accept-Encoding: gzip, deflate, br, zstd by default and decodes Content-Encoding: br responses automatically |
node:zlib | brotliDecompress, brotliDecompressSync, createBrotliDecompress |
DecompressionStream | With the brotli format |
Bun adds two points worth repeating. Executables built with bun build and its compile option embed the runtime, so they stay affected until rebuilt. And for fetch over an unencrypted connection, an attacker in the network path can also supply the response. Bun says it has not tested an attack against Bun and tells users to treat the issue as a remote code execution risk. Bun's fix is pull request #46392.
Envoy and Istio specifics
Envoy's advisory says the impact is limited to "Envoy deployments that have Brotli decompressor configured." Istio's bulletin says users "who enable Brotli decompression, for example with an EnvoyFilter, should upgrade." Envoy's documentation places decoding in the decompressor HTTP filter (envoy.filters.http.decompressor), which supports gzip, Brotli and zstd libraries and can run on requests, responses or both.
The Istio bulletin's headline score of 7.7 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:H) covers the bulletin as a whole, which includes several issues; it is not a score for the Brotli flaw. Istio's next patch releases on the same branches, 1.29.10, 1.30.7 and 1.31.3 (22 October), add the eMudhra trust store changes from bulletin ISTIO-SECURITY-2026-008.
Indicators of compromise
No primary source has published indicators of compromise: no IP addresses, domains, hashes or request signatures. CISA's SSVC entry records exploitation as "none" on 23 October. According to a security vendor's research, code execution was reproduced against an unmodified Node.js 24.22.0 Linux arm64 image, taking multiple attempts. We have seen no equivalent public claim for Python, Envoy, Istio, Bun or curl.
What you can watch for:
- Crashes (segmentation faults, aborts) in processes that decode Brotli, especially Python workers, Envoy sidecars and Bun services. A crash is not proof of an attack, but it is worth a look.
- Requests carrying
Content-Encoding: brto endpoints that never legitimately receive compressed uploads. - After patching, Brotli decode failures with error code
-17.
Python: the part scanners missed
Python is where this incident is easiest to miss, for three reasons.
Nobody installs Brotli on purpose. It usually arrives as an optional extra, such as urllib3[brotli] or httpx[brotli], or as a transitive dependency. The urllib3 documentation says that when either the brotli or brotlicffi package is installed, br is added to the default Accept-Encoding header. The PyPA entries say that with Brotli installed, urllib3 and requests, httpx with the Brotli extra and aiohttp decode Brotli responses automatically. Installing the package changes the behaviour of code that never imports it.
The fix was quiet. The Brotli 1.2.1 and brotlicffi 1.2.1.0 releases on PyPI did not explain themselves at upload time. A public write-up found the change by diffing the source distributions.
The data lagged. GitHub's advisory for the PyPI packages went live on 22 October without a CVE ID in its identifier field. As of 23 October, a security vendor's report said that the CVE, OSV and GHSA records did not map the CVE to PyPI, and that common Python audit tools might therefore not flag it. By 24 October that had changed: the PYSEC entries carry the CVE as an alias, OSV's CVE record lists both PYSEC IDs, and an OSV query for PyPI brotli 1.2.0 returns both GHSA-9v8h-qqgm-4m35 and PYSEC-2026-4322. Whether a given tool flags it now depends on when that tool last pulled its data. Check by hand.
Find it
# Is either package installed in this environment?
python -m pip show Brotli brotlicffi 2>/dev/null | grep -E '^(Name|Version|Required-by)'
# Version from inside Python
python -c "import brotli; print(brotli.__version__)"
python -c "import brotlicffi; print(brotlicffi.__version__)"
# Who pulled it in?
python -m pip install pipdeptree
pipdeptree --reverse --packages Brotli,brotlicffi
# Lockfiles across a repo (pip, Poetry, uv, Pipenv, conda)
grep -rniE '^(brotli|brotlicffi)[ =<>~!]|name = "(brotli|brotlicffi)"' \
--include='requirements*.txt' --include='poetry.lock' --include='uv.lock' \
--include='Pipfile.lock' --include='environment*.yml' .
# uv and Poetry environments
uv pip list | grep -i brotli
poetry show | grep -i brotli
Do not stop at your application's virtual environment. Check:
- The system Python on hosts and in base images (
/usr/lib/python3*/site-packages,/usr/lib/python3/dist-packages). - Distribution packages such as Debian's
python3-brotli, which may build against the system libbrotli (dpkg -l | grep -i brotli). - Conda environments, which can carry their own Brotli builds (
conda list | grep -i brotli). - Jupyter and data science images, which often carry
aiohttpand its speedups. - AWS Lambda layers and other zipped deployment bundles.
Fix it
python -m pip install --upgrade "Brotli>=1.2.1" "brotlicffi>=1.2.1.0"
Then pin the floor in whatever controls your builds:
# requirements.txt or constraints.txt
Brotli>=1.2.1
brotlicffi>=1.2.1.0
With Poetry, uv or pip-tools, raise the same floors and regenerate the lockfile. If you only need one of the two packages, consider removing the other. Restart every worker afterwards. A Gunicorn, Celery or uWSGI process that loaded the old extension keeps it in memory until it exits.
If you cannot upgrade today
A security vendor's report recommends uninstalling both packages where Brotli support is optional, removing extras such as httpx[brotli] that would pull them back, and regenerating lockfiles. It notes that requests, httpx and urllib3 keep gzip and deflate support without them, but a server that sends Brotli anyway may then produce an unreadable response or an error.
The same report makes a point that matters for outbound traffic: removing br from Accept-Encoding is not enough on its own, because a hostile server can send a Brotli response regardless, and urllib3 picks its decoder from the response's Content-Encoding header. If you keep the package installed, check the header before the body is decoded:
import requests
def fetch_without_brotli(url: str) -> bytes:
headers = {"Accept-Encoding": "gzip, deflate"}
with requests.get(url, headers=headers, stream=True, timeout=10) as r:
encoding = r.headers.get("Content-Encoding", "").lower()
if "br" in [e.strip() for e in encoding.split(",")]:
raise ValueError("refusing Brotli-encoded response until Brotli is patched")
return r.content
This is a stopgap for code you control. It does nothing for libraries that make their own requests.
Envoy and Istio
Most service meshes never decode Brotli, because the decompressor filter is opt-in. That is the good news. The bad news is that the configuration that turns it on can live in many places: Envoy bootstrap files, xDS from a control plane, Istio EnvoyFilter resources, gateway Helm values, or a platform team's shared config.
Find out whether you decode Brotli
# Envoy version on a standalone proxy
envoy --version
# Look for a Brotli decompressor in a running Envoy's config (admin port)
curl -s http://127.0.0.1:9901/config_dump | grep -iE 'brotli|decompressor'
# Istio: control plane and data plane versions
istioctl version
# Istio: any EnvoyFilter mentioning Brotli or the decompressor
kubectl get envoyfilter -A -o yaml | grep -iE -B3 -A3 'brotli|decompressor'
# Istio: check a specific sidecar or gateway's live config
istioctl proxy-config listener deploy/istio-ingressgateway -n istio-system -o json \
| grep -iE 'brotli|decompressor'
If none of these turn up a Brotli decompressor, the advisories say you are not exposed through Envoy. Patch anyway, because configs change.
Upgrade
| Platform | Move to | Notes |
|---|---|---|
| Envoy 1.40 | v1.40.0 or later | Newer patch releases exist in every branch; take the latest |
| Envoy 1.39 | v1.39.3 or later | |
| Envoy 1.38 | v1.38.6 or later | |
| Envoy 1.37 | v1.37.8 or later | |
| Envoy 1.36 | v1.36.12 contains the fix | Branch unsupported per a security vendor's report; plan a move |
| Istio 1.31 | 1.31.2 or later (1.31.3 also carries the eMudhra trust store fix) | |
| Istio 1.30 | 1.30.6 or later (1.30.7 also carries the eMudhra fix) | |
| Istio 1.29 | 1.29.9 or later | Istio says 1.29 builds with an unsupported Go line; move to 1.30 or 1.31 |
For Istio, upgrading the control plane is not enough. Sidecars keep the old proxy image until pods restart:
# Which proxy images are running?
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{range .spec.containers[*]}{.image}{" "}{end}{"\n"}{end}' \
| grep -E 'proxyv2|envoy'
# After upgrading istiod, restart workloads so they get the new sidecar
kubectl rollout restart deployment -n my-namespace
Do the same for gateways, waypoints and any Envoy you run outside Istio: API gateways, edge proxies, and products that embed Envoy. For products that embed Envoy, the fix has to come from that product's vendor.
If you cannot upgrade today
Remove the Brotli decompressor from the filter chain, or delete the EnvoyFilter that adds it. Envoy's documentation notes that you can install separate decompressor filters for requests and for responses, so check both directions. If clients legitimately send Brotli bodies, reject them at a load balancer in front of the mesh until you patch, using the edge rules further down.
Bun
bun --version
bun upgrade # to 1.4.4 or later
In containers, pin the fixed image tag and rebuild. In CI, bump the version in your setup step. Then find compiled executables:
# Repos that build standalone binaries with Bun
grep -rn --include='package.json' --include='*.sh' --include='*.yml' -E 'bun build .*compile' .
Each of those binaries embeds the runtime it was built with. Rebuild with 1.4.4 or later and redistribute them. If you cannot upgrade, Bun's advisory offers a per-request workaround for fetch: pass decompress: false, which stops Bun advertising br and stops it decoding the response. Bun says there is no process-wide workaround.
curl and libcurl
curl does not contain its own Brotli decoder. It links libbrotli when built with Brotli support, so the fix arrives through the library. We found no curl project advisory for this CVE in the sources we reviewed; the security vendor report's guidance is to update curl's Brotli dependency to 1.2.1 or a patched older release, or take your vendor's security-fixed package, and to rebuild anything that links it statically.
# Does this curl support Brotli?
curl -V | grep -io brotli
# Which libbrotli does it load?
ldd "$(command -v curl)" | grep -i brotli # Linux
otool -L "$(command -v curl)" | grep -i brotli # macOS
# libcurl build features
curl-config --features | grep -i brotli
libcurl only decodes compressed responses when the application asks it to, through its accept-encoding option or the command line's compressed transfer flag. Scripts that never ask for compression are less exposed. Applications that enable it for every transfer, including many language bindings and package managers, are exposed to any server they talk to.
Operating systems and container images
This is the long tail. Most Linux distributions ship libbrotli as a shared library, and curl and many other tools can link it.
# Debian and Ubuntu
dpkg -l | grep -iE 'libbrotli|brotli'
apt-cache policy libbrotli1
# RHEL, Fedora, Amazon Linux
rpm -qa | grep -i brotli
# Alpine
apk info -v 2>/dev/null | grep -i brotli
# macOS with Homebrew
brew list --versions brotli
# Every shared copy of the decoder on a host
find / -xdev -name 'libbrotlidec*' 2>/dev/null
The Debian tracker shows the pattern to expect from distributions: fixed packages carry distro version strings, not upstream ones. Debian 12's fix is 1.0.9-2+deb12u1 and Debian 13's is 1.1.0-2+deb13u1. A version check against "1.2.1" would flag both as vulnerable even though they are fixed. Use your distribution's advisory as the source of truth.
The tracker also notes that Debian's nodejs package "uses system provided brotli library," so on Debian the Node fix comes through the brotli package. The same may be true on other distributions.
Static copies are the hard part. Applications that vendor Brotli source and compile it in will not show up in dpkg or rpm. Look for them in images:
# Search an image for Brotli decoder symbols in shared objects and binaries
docker run --rm --entrypoint sh myimage:tag -c \
'for f in $(find / -xdev -type f \( -name "*.so*" -o -perm -u+x \) 2>/dev/null); do
grep -l "BrotliDecoderDecompressStream" "$f" 2>/dev/null; done'
A hit tells you a binary contains a Brotli decoder. It does not tell you the version, so check with the vendor or rebuild from patched source.
After updating packages, restart processes or reboot. A long-running process keeps the old shared library mapped until it exits:
# Processes still using a deleted (replaced) libbrotli
sudo lsof 2>/dev/null | grep -i 'libbrotli' | grep -i deleted
Then rebuild every image whose base layer includes libbrotli, and redeploy. Our post on container image supply chain incidents in 2026 covers why "patched in the registry" and "patched in production" are different states.
Edge mitigations for both directions
If you cannot patch everything this week, put controls at the edges, keeping in mind that each one covers only one direction.
Inbound (protects servers). Reject requests whose Content-Encoding contains br, matched case-insensitively and inside comma-separated lists, at the load balancer or reverse proxy, before any decoder sees the body.
# nginx: refuse Brotli request bodies
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*(,|$) }
Outbound (protects clients). Remove br from Accept-Encoding and also refuse Brotli responses before decoding. An egress proxy that strips or blocks Content-Encoding: br responses can do this for many services at once, as long as it does not decode Brotli itself with a vulnerable library.
Neither direction. Code that decompresses files or messages with a Brotli library is not covered by any HTTP control. Find it with a search for imports of brotli, brotlicffi, zlib.brotliDecompress and DecompressionStream.
These controls can break legitimate traffic, so log before you block, and remove them once you have patched.
What changed since first reports
| First reports (21 to 22 Oct) | Now (24 Oct) |
|---|---|
| Framed as a Node.js bug, "Node2Shell" | Framed as a Brotli library bug, "HTTP2Shell," reaching Python, Envoy, Istio, Bun, curl and OS packages |
| Node.js rated it High with no CVSS score | The CNA published CVSS 4.0 9.2 Critical; Ubuntu set Medium priority |
| No Python advisory | GHSA-9v8h-qqgm-4m35, PYSEC-2026-4322 and PYSEC-2026-4323 published; OSV maps the CVE to PyPI |
| No mention of meshes | Envoy and Istio confirm fixes that shipped on 6, 13 and 15 October |
| No distro fixes | Debian has fixed bookworm and trixie; unstable and Ubuntu still pending |
| Exploitation unknown | CISA SSVC records "none"; not in KEV; code execution reproduced in research against one Node build |
| Discovery timeline unclear | The CVE record credits researchers at OpenAI; the fix commit is dated February, but no primary source says when the flaw was first reported |
Open questions
- Browsers and Electron. Both ship their own copies of Brotli. We have not seen a browser vendor statement on this CVE in the sources we reviewed.
- Other bindings and ports. The Brotli repository also holds a Go cgo binding, which links the C library through pkg-config per the 1.2.0 release notes, and pure Java and Kotlin decoders. The fix commit changes only the C decoder, and upstream has not said whether other decoders share the logic flaw.
- Packages that vendor C Brotli. Extensions for Ruby, PHP, Rust and other ecosystems that compile the C decoder will need their own releases. We found no consolidated list.
- Managed services. CDNs, API gateways and serverless platforms that decode Brotli have to confirm their own status. Ask your providers.
What would have helped
Nothing on this list would have stopped a logic bug in a C decoder. What decides your exposure is how quickly you can answer "where do we decode Brotli?" and how quickly you can ship the fix once it exists.
- Track optional extras and native extensions. An inventory that records
Brotli1.2.0 even when it came in through an extra three levels down turns this incident into a lookup. Include the Python environments inside images. - Track shared libraries in images. Record OS packages such as
libbrotli1in your image SBOMs, and rebuild images when a base package changes. Vendored copies need a list of their own. - Know which services decode untrusted compressed data. Keep a short list: services that accept compressed uploads, gateways with decompressor filters, and anything that fetches URLs chosen by users or by the internet. Patch those first.
- Treat proxies as dependencies. Mesh and gateway versions belong in the same inventory as application dependencies.
- Do not wait for your scanner's data to catch up. When an advisory names a C library, ask which of your packages embed it instead of waiting for each ecosystem's database.
- Restart after patching. Shared libraries and native extensions stay mapped in running processes. Make "restart or redeploy" part of the patch step, not a follow-up.
Where Safeguard fits
Safeguard does not patch your Envoy fleet, change your proxy configuration or push OS updates to your hosts. Those steps are yours. Here is where it helps.
- Inventory repos, images and SBOMs.
safeguard scan --dir .scans a repository,safeguard scan --image <name>scans a container image andsafeguard scan --sbom <file>scans an SBOM file, with JSON results under./output. That shows which components are present, including transitive ones such asBrotliarriving through extras. Whether this CVE is matched depends on the vulnerability data and on the component being identified, so confirm with the version checks above. - Find every project with a component. The platform shows which projects contain a given component and version. Our post on using SBOMs to find affected products fast walks through the approach.
- Prioritise with reachability. Vulnerability matching against vulnerability databases, plus reachability analysis, helps separate the services that actually call into a decoder from the long tail.
- Gate the rebuild. Policies and gates evaluated in the platform, and
safeguard execin CI, can stop a build that still carries a known vulnerable component from shipping. - Make the change with approval. The Safeguard CLI's assistant can edit files and run commands with your approval. Ask it to raise the
Brotlifloor inrequirements.txtorpyproject.toml, regenerate the lockfile, bump a Bun version or an Istio chart version, and run your tests. Set/permissionsto Ask or Strict so every command waits for you.
Zero-Day Discovery and Gold
- Zero-Day Discovery looks for previously unknown flaws before a CVE exists, in an agentic mode and a deterministic scanner mode, and issues findings as SGZ advisories before they reach public CVE databases. It covers seven package ecosystems. The flaw here sits in a C library compiled into runtimes, proxies and native extensions, and we make no claim that Zero-Day Discovery found it or that any SGZ advisory covered it.
- Gold Open Source (free, no login) and Gold Registry (paid, hardened and signed components and images) are described in the next section. We do not claim Gold carried a patched Brotli build before the public fix. The habit they support is the one this incident rewards: pull patched, signed components from a curated source and rebuild fast.
Common mistakes
- Checking
requirements.txtand stopping. Brotli usually arrives through extras or transitive dependencies. Check the installed environment and the lockfile. - Upgrading Brotli but not brotlicffi. They are separate packages with separate fixed versions, 1.2.1 and 1.2.1.0.
- Comparing distro versions to upstream ones. Debian's fixed
1.0.9-2+deb12u1looks older than 1.2.1 but carries the fix. - Patching Istio's control plane only. Sidecars and gateways keep the old proxy until they restart.
- Assuming Envoy is safe because it is behind a CDN. If Envoy decompresses upstream responses, the upstream is the attacker in that direction.
- Stripping
brfromAccept-Encodingand calling it done. A hostile server can send Brotli anyway; refuse it before decoding. - Relying on a scanner that has not refreshed. The PyPI mapping arrived a day after the fix. Check by hand for this one.
FAQ
Is HTTP2Shell a different vulnerability from Node2Shell?
No. Both names refer to CVE-2026-111497, the out-of-bounds write in Google's Brotli decoder. "Node2Shell" came first and focused on Node.js. "HTTP2Shell" is the newer name from a security vendor's report, reflecting that any software decoding Content-Encoding: br can be affected.
Is my Python app affected if I never import brotli?
Possibly. If the Brotli or brotlicffi package is installed, urllib3 adds br to its default Accept-Encoding header, and the PyPA entries say urllib3 and requests, httpx with its Brotli extra and aiohttp then decode Brotli responses automatically. Check whether either package is installed, and upgrade to 1.2.1 or 1.2.1.0.
Is Istio affected by default?
The advisories limit Envoy's exposure to deployments with a Brotli decompressor configured, and Istio points to users who enable Brotli decompression, for example through an EnvoyFilter. Upgrade to 1.29.9, 1.30.6, 1.31.2 or later anyway, and restart sidecars.
Is this being exploited in the wild?
As of 24 October, no source we reviewed reports exploitation in the wild. CISA's SSVC assessment on the CVE record says "none," and the CVE is not in KEV. Researchers reproduced code execution against one unmodified Node.js build, which is a reason to move quickly.
Why did my vulnerability scanner not flag the Python packages?
The GitHub advisory for the PyPI packages was first published without a CVE ID attached, and the PyPA entries that tie the CVE to PyPI appeared a day later. Tools that had not refreshed their data in between could miss it. Check by hand and update your tool's database.
Do I need to do anything for nginx or Apache?
According to a security vendor's report, the standard Apache mod_brotli only compresses and nginx does not decode br by default, so neither is affected in its default form. Third-party modules or custom builds that decode Brotli are a different matter. The OS libbrotli package on the same host still needs its update.
Was the fix really available since February?
The commit that fixes the bug, 0ed4268a, is dated 27 February 2026 in the Brotli repository. No Brotli release shipped with that commit until the coordinated October releases.
Fix it without waiting for upstream
For HTTP2Shell the upstream fixes now exist, so the gap that matters is the first one below: how long the fixed Brotli takes to reach every Python environment, proxy and image you run.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would have helped
None of these demos shows this CVE; each shows a real flow on another example, and the notes explain how it applies here.
The CLI demo fixes a lodash CVE: the assistant reads the finding, asks before each command, edits package.json, installs and runs the tests. For HTTP2Shell you would run the same session in a Python service and ask it to raise Brotli to 1.2.1 and brotlicffi to 1.2.1.0, regenerate the lockfile and run the tests, approving each command.
Click through it at your own pace, or open it full screen.
The desktop app demo sends work to your computer, picks a folder, and asks you to Approve or Deny each command and file edit, ending with an upgrade done and tests passing. Here that could be the Istio chart bump in your platform repo or the Bun version in a Dockerfile.
Click through it at your own pace, or open it full screen.
The runner demo sets up a runner on your own machine, approves it with its device code, and scans from the terminal. For this incident, you would use it to scan your Python worker images and gateway images on a build host.
Click through it at your own pace, or open it full screen.
To try it on your own services, download Safeguard and follow the CLI docs.
For more context on this month's incidents, see our breakdown of PHP2Shell, CVE-2026-109618 and CVE-2026-109619 and our look at AI-found vulnerabilities behind PHP2Shell and PwnEscape.
Quick check
Your Python service has requests installed but no brotli line in requirements.txt. Can it still be exposed?
Yes. If Brotli or brotlicffi is installed in the environment, for example through an extra or a transitive dependency, urllib3 advertises br and decodes Brotli responses automatically. Check the installed environment with pip show, then upgrade to 1.2.1 or 1.2.1.0.
You upgraded istiod to 1.31.2. Are your sidecars fixed?
Not until they restart. Sidecars and gateways keep running the old proxy image until their pods are recreated, so roll out restarts and confirm the running proxy versions afterwards.
Sources
- Google Brotli releases v1.2.1, v1.1.1, v1.0.10
- Google Brotli fix commit 0ed4268a
- CVE record CVE-2026-111497
- OSV record CVE-2026-111497
- GitHub advisory GHSA-9v8h-qqgm-4m35 (PyPI brotli and brotlicffi)
- PyPA advisory PYSEC-2026-4322 (Brotli)
- PyPA advisory PYSEC-2026-4323 (brotlicffi)
- PyPI: Brotli
- PyPI: brotlicffi
- Envoy advisory GHSA-3wvj-j6qm-62mr
- Envoy releases
- Istio security bulletin ISTIO-SECURITY-2026-007
- Istio 1.31.2 release notes
- Istio 1.31.3 release notes
- Istio security bulletin ISTIO-SECURITY-2026-008
- Istio security bulletins index
- Bun advisory GHSA-663w-cv8g-jj2p
- Bun releases
- Node.js October 2026 security releases
- Debian security tracker: CVE-2026-111497
- Ubuntu CVE tracker: CVE-2026-111497
- CISA Known Exploited Vulnerabilities catalog
- urllib3 documentation: make_headers and accept_encoding
- Patch the Planet: Brotli
- A security vendor's report on HTTP2Shell and Python packages
- A security vendor's report on Node2Shell in Node.js
- A security vendor's research on Brotli data to code execution
- A public write-up on the Python packages
Last reviewed: 24 October 2026. This is a developing story; check the linked advisories for the latest status.