DEV Community

Cover image for HTTP2Shell (CVE-2026-111497): pip-audit Missed Brotli. How to Check Your Stack
Alan West
Alan West

Posted on Originally published at blog.authon.dev

HTTP2Shell (CVE-2026-111497): pip-audit Missed Brotli. How to Check Your Stack

If you ran pip-audit on Thursday, got a clean report and moved on, run it again. CVE-2026-111497 is an out-of-bounds write in Google's Brotli decoder. Aikido's headline now calls it HTTP2Shell; the rest of the internet mostly knew it as Node2Shell. It has a CVSS v4 score of 9.2 and affects Brotli 1.0.3 through 1.2.0. As of October 23, Aikido reported that pip-audit, Dependabot and Safety were not flagging the vulnerable Python packages, because the advisory databases hadn't mapped the CVE to Brotli or brotlicffi on PyPI.

That mapping has landed. Scanners are still a bad way to answer "am I exposed?" for this bug, because most of the vulnerable copies aren't Python packages at all. These are the checks I'd run instead, for Python, Node, Bun and Docker images, plus the fixed versions.

What CVE-2026-111497 actually is

The bug sits in c/dec/decode.c. In the vulnerable versions, DecodeBlockTypeAndLength() returns a plain boolean, so BROTLI_FALSE can mean either "this stream is malformed" or "I need more input". The caller can't tell those apart. The Node.js advisory describes what happens next: an invalid block switch gets reported as a temporary need for more input, and the streaming decoder keeps appending bytes to an eight-byte internal buffer, writing past the end of it. Node lists crash, memory disclosure and remote code execution as possible outcomes.

The upstream fix is google/brotli commit 0ed4268. It adds a new error code, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (-17). According to the Aikido write-up, a researcher diffed 1.2.0 against 1.2.1 and found only the version bump plus that commit's decoder changes, then built both with AddressSanitizer. 1.2.0 consumed a roughly 23 MB crafted stream and kept asking for more, even when fed one byte at a time. 1.2.1 rejected it with -17 every time.

Node credits OpenAI Codex Security (Khai Tran and Sylvie Mayer) with the report. Aikido says OpenAI found it in February and that the root cause was fixed in the Brotli source back then without anyone treating it as a security issue, so no release carried the fix until now. Commenters in the HN thread doubted the February date, so I'd treat it as unconfirmed.

You don't need HTTP to trigger it, but that's how you'll get hit

The HN thread spent a lot of energy mocking "HTTP2Shell". It's a decompressor bug, and as pimterry pointed out there, you can trigger it with no HTTP involved by decoding Brotli yourself. Someone linked a rival name, Broccoli2Shell, which already has its own site. I like that one better.

Still, most real exposure comes through HTTP content negotiation. Anything that decodes Content-Encoding: br from a peer it doesn't trust can be hit, and that works in both directions:

  • Inbound: a malicious client sends a Brotli-compressed request body to a server that decompresses it. Node apps with body parsing enabled are in this group, and so is ASP.NET with request decompression turned on.
  • Outbound: a malicious or compromised server sends a Brotli response to a client that decodes it automatically. In Python that means requests, httpx and aiohttp.

On HN, bawolff argues the outbound case is much less severe because the attacker first has to get your code to call their server. That's fair for a payment service that only ever calls Stripe. It's not fair for scrapers, webhook senders, link previewers, RSS fetchers, or anything that does requests.get(user_supplied_url). If your service fetches URLs that users give it, the attacker controls the server.

Python: requests decodes Brotli if it's merely installed

urllib3, and therefore requests, decodes Brotli whenever Brotli or brotlicffi happens to be importable. You don't opt in. In a venv with Brotli 1.1.0, requests 2.34.2 and urllib3 2.8.0, this is what requests sends by default:

>>> requests.utils.default_headers()["Accept-Encoding"]
'gzip, deflate, br'
Enter fullscreen mode Exit fullscreen mode

Uninstall Brotli and the br disappears. httpx does the same with the brotli extra, aiohttp does it when Brotli is installed, and Aikido lists Scrapy and anything built on these clients. The Python Brotli package gets 82.8 million downloads a month, and I doubt many of those come from people who typed pip install brotli themselves.

So check whether anything in the environment dragged Brotli in, and which version it brought. Your requests version won't tell you that.

How to check for vulnerable Brotli in 5 minutes

Python, in every venv, CI runner and image you care about:

pip show Brotli brotlicffi
Enter fullscreen mode Exit fullscreen mode

You're vulnerable if Brotli is below 1.2.1 or brotlicffi is below 1.2.1.0 (GHSA-9v8h-qqgm-4m35). A WARNING: Package(s) not found line for one of them is fine.

To find out who pulled it in:

pipdeptree -r -p Brotli
pipdeptree -r -p brotlicffi
Enter fullscreen mode Exit fullscreen mode

Lockfiles are faster than spinning up every environment:

grep -i brotli poetry.lock uv.lock requirements*.txt 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

Container images are where it tends to turn up. Override the entrypoint so the image's own startup command doesn't get in the way:

docker run --rm --entrypoint pip <image> show Brotli brotlicffi
Enter fullscreen mode Exit fullscreen mode

Node and Bun:

node -v   # need 22.23.4, 24.22.1 or 26.12.1+
bun -v    # need 1.4.4+
Enter fullscreen mode Exit fullscreen mode

Node 20 reached end of life on April 30, 2026, and EOL lines don't get security releases. Node bundles its own Brotli, so your OS package doesn't matter there. The same goes for single-executable apps and compiled Bun binaries: they carry their own copy and have to be rebuilt.

Fixed versions

Component Fixed in Gotcha
Node.js 22.23.4, 24.22.1, 26.12.1 Rebuild images and SEA binaries
libbrotli 1.2.1 (or 1.0.10 / 1.1.1 on older lines) Distros may backport without a version bump
Python Brotli 1.2.1 Regenerate lockfiles
brotlicffi 1.2.1.0 Same
Bun 1.4.4 Recompile bun build --compile outputs
Envoy 1.40.0, 1.39.3, 1.38.6, 1.37.8 Sidecars update with the mesh, not your app
Istio 1.29.9, 1.30.6, 1.31.2 Check the Envoy version it ships
curl / libcurl Whatever your vendor ships against Brotli 1.2.1 Static builds need a rebuild

For Python:

pip install -U "Brotli>=1.2.1"
pip install -U "brotlicffi>=1.2.1.0"
Enter fullscreen mode Exit fullscreen mode

Then regenerate the Poetry, uv or pip-tools lockfile and rebuild every image that contains it. Updating the package on your laptop doesn't patch the container that shipped last Tuesday.

Does pip-audit catch CVE-2026-111497 now?

For the PyPI packages, yes. The OSV entry now lists brotli and brotlicffi with aliases PYSEC-2026-4322 and PYSEC-2026-4323. The advisory was published on October 22 and last modified on October 24, 2026. If your weekly scan ran before the mapping showed up, you have a green checkmark sitting on top of a 9.2.

My bigger complaint is about everything pip-audit was never going to see:

  • the Brotli statically linked into your Node binary
  • the copy inside a compiled Bun executable
  • Envoy sidecars in your Istio mesh
  • libbrotli under a statically built curl
  • distro packages where the fix was backported and the version string didn't change (Ubuntu's libbrotli1 fix was still under review at medium severity when the HN thread was going)

People in the HN thread also listed Git for Windows, Wireshark, Visual Studio and .NET as shipping Brotli. A Python dependency scanner reports on Python dependencies, and this bug lives in a C library that roughly half your stack vendors.

Workaround if you can't patch today: drop br

  • Inbound: reject any request whose Content-Encoding contains br at the proxy, load balancer or middleware, before anything decodes it, and match case-insensitively. Nginx and Apache don't decode Brotli request bodies out of the box (Apache's mod_brotli only compresses), which makes them a reasonable place to block it.
  • Outbound, Python: uninstall Brotli and brotlicffi, remove extras like httpx[brotli], regenerate locks and restart. Clients fall back to gzip and deflate. This does nothing for Brotli bundled into other runtimes.
  • Outbound, generally: removing br from Accept-Encoding is not enough. A hostile server can send Brotli whether you asked for it or not, so you have to reject br responses before the body gets decoded automatically.

Someone on HN asked whether a TLS-terminating load balancer in front of a Node app protects you. The answer in the thread was no: terminating TLS has nothing to do with decoding bodies, and most load balancers pass the body through untouched. Unless yours is explicitly configured to drop br, assume it does nothing.

The Hacker News (the news site, not the forum) reported no exploitation in the wild as of October 22. Aikido also notes that researchers have already exploited the same decoder code against an unmodified Node.js 24.22.0 build, so I wouldn't count on that staying true for long.

Did your scanner flag Brotli, or did you find it in a container image by hand? I'm curious how many copies people turned up compared to the number of services they actually run.

Sources

Top comments (0)