HTTP2Shell (CVE-2026-111497): Is Your App Affected, and What to Patch

Last reviewed
9 min read

TL;DR: HTTP2Shell (CVE-2026-111497, first called Node2Shell) is an out-of-bounds write in Google's Brotli decoder, reachable through Node.js, Python HTTP clients, curl, Bun and some proxies whenever they decompress Brotli data an attacker controls. Your app is exposed if it decodes Brotli request bodies, fetches URLs someone else chooses, or decompresses uploads; the fix is Node.js 22.23.4, 24.22.1 or 26.12.1, Brotli 1.0.10, 1.1.1 or 1.2.1 (Python brotlicffi 1.2.1.0) and Bun 1.4.4.

If you run a Node.js or Python service, there is a good chance it carries a copy of the Brotli decoder that was found to corrupt memory on crafted input. This brief is for the people who own those services: what the bug is, which of your code paths can reach it, how to check your own app in a few minutes, and what to upgrade first. Every check below is for systems you own or have written permission to test.

What is HTTP2Shell, and why does it have two names?

CVE-2026-111497 is an out-of-bounds write in Google's Brotli decompression library. On 21 October 2026 the Brotli project shipped fixed releases 1.0.10, 1.1.1 and 1.2.1, and Node.js shipped security releases 22.23.4, 24.22.1 and 26.12.1 the same day. Node.js credits the report to OpenAI Codex Security.

It was first called Node2Shell, because Node.js bundles the decoder and was the first runtime to disclose. Within two days it became clear that the same decoder sits inside Python packages, HTTP clients and proxies, and Aikido renamed it HTTP2Shell to reflect that. It is one bug and one CVE. Despite the name, it has nothing to do with the HTTP/2 protocol: the trigger is Brotli-compressed data, over any HTTP version or none.

Sources disagree on severity, and both ratings are worth knowing. Google, as the CVE numbering authority, scored it 9.2 Critical under CVSS 4.0, the rating the GitHub advisory carries since its review on 23 October; the Node.js advisory rates it High. Treat it as the higher of the two until you know none of your code paths reach the decoder.

HTTP2Shell timeline: decoder fix committed in February, released 21 Oct, proof of concept 22 Oct, renamed 23 Oct

The fix sat in the Brotli repository for almost eight months before anyone connected it to memory corruption; from release to public proof of concept took one day.

How the decoder overflows

Brotli splits compressed data into blocks and can switch block types mid-stream. In vulnerable versions, the function that decodes a block switch returned a plain boolean, so "this stream is malformed" and "I need more input" looked the same to its callers. In the Node.js advisory's words, "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."

Vulnerable Brotli treats a failed block switch as a need for more input and overflows; the fix rejects it

The fix makes the block-switch function return an error code, and the decoder rejects the stream with BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH (−17).

The upstream change is small and was written in February 2026, months before anyone connected it to memory corruption and shipped a release. That gap is why so many products were still on 1.2.0 or older in October.

What an attacker gets depends on the process. The Node.js advisory lists a crash, disclosure of process memory, or remote code execution. Public reporting so far points to the first two being the common outcome: HeroDevs summarises a first public proof of concept that reached code execution only on a deliberately weakened build, and a second report of code execution on the official Node.js 24.22.0 Linux arm64 image that did not succeed on every attempt. As of 24 October 2026, when this brief was last reviewed, no exploitation in the wild had been reported and the CVE was not in CISA's Known Exploited Vulnerabilities catalog (its 23 October edition). Plan for the patch, not for the exploit's current reliability.

Which parts of my app reach the vulnerable decoder?

Three code paths can feed attacker-controlled Brotli to the decoder: inbound request bodies, outbound responses, and your own decompression calls. The decoder only matters if data an attacker controls flows into it.

Inbound request bodies, outbound responses and app code all feed untrusted Brotli to the decoder

  1. Inbound request bodies. A client sends Content-Encoding: br, and your framework decompresses the body before your handler sees it. Cycode lists Express 5 with body-parser 2.x (json(), urlencoded(), text(), raw()), the Koa body parsers and @fastify/compress with request decompression. Express 4 and body-parser 1.x do not decode Brotli bodies, and neither does a bare node:http server.
  2. Outbound responses. Your server calls a URL it does not fully control — a webhook target, a link-preview fetcher, a user-supplied import URL — and the response comes back Brotli-encoded. Node's built-in fetch() decodes it automatically, as do Python's urllib3 and requests, httpx with the brotli extra and aiohttp when a Brotli package is installed. Sending a narrower Accept-Encoding does not protect you: a malicious server can send Brotli anyway.
  3. Your own code calls node:zlib's brotliDecompress, brotliDecompressSync or createBrotliDecompress, or Python's brotli.decompress(), on data a user uploaded.

Nginx and Apache do not decode Brotli request bodies by default; mod_brotli compresses responses. Envoy and Istio can, through their decompressor filters, and both have fixed releases.

If you are not sure which of your endpoints accept request bodies at all, list them before you test. AppMap crawls your app and lists every API endpoint with the authentication it requires, which gives you the set to check in the next step; mapping the attack surface first explains why that order saves time.

How do I check if my app is affected?

Check which Brotli your runtime carries, whether your API decodes Brotli request bodies, and whether your server fetches URLs someone else chooses — three checks, in order of how much they tell you, and none of them sends a malformed stream.

1. Which Brotli does your runtime carry?

Node.js reports the Brotli version it was built with:

node -p "process.version + ' brotli ' + process.versions.brotli"
v26.7.0 brotli 1.2.0

Anything that prints a Brotli version from 1.0.3 to 1.2.0 is a vulnerable build. A fixed Node.js release prints a newer Brotli, but compare the Node version itself against 22.23.4, 24.22.1 or 26.12.1 — that is what the advisory names. Run the same command inside each production image, since every image carries its own runtime:

docker run --rm --entrypoint node your-registry.example/your-app:latest \
  -p "process.version + ' brotli ' + process.versions.brotli"

For Python, check every environment and image for the two packages that compile the decoder:

pip show Brotli brotlicffi 2>/dev/null | grep -E '^(Name|Version)'
pipdeptree -r -p Brotli      # what pulled it in

And curl, which often links the system libbrotli:

curl -V | head -1 | grep -o 'brotli/[0-9.]*'

2. Does your API decompress Brotli request bodies?

Send a small, valid Brotli-compressed JSON body to an endpoint that accepts JSON, and compare the response with the same body sent uncompressed. Use your own app (your-app.example below) or CyberGym.

node -e "process.stdout.write(require('node:zlib').brotliCompressSync(JSON.stringify({email:'probe@your-app.example',password:'x'})))" > ping.br
printf '%s' '{"email":"probe@your-app.example","password":"x"}' > plain.json

curl -s -o /dev/null -w '%{http_code}\n' -X POST https://your-app.example/api/login \
  -H 'Content-Type: application/json' -H 'Content-Encoding: br' --data-binary @ping.br
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://your-app.example/api/login \
  -H 'Content-Type: application/json' --data-binary @plain.json

Here is what it looks like against the login endpoint of CyberGym, a deliberately vulnerable practice app that runs on Express 4:

415    UnsupportedMediaTypeError: unsupported content encoding "br"
401    Invalid email or password.

The 415 means the server refuses to decode Brotli request bodies at all, so the inbound path does not reach the decoder. If your app answers the Brotli request the way it answers the plain one — a 401, a 400 validation error, a 200 — it decoded the body, and that endpoint is on the inbound path until you upgrade.

3. Does your server fetch URLs someone else chooses?

Search your code for outbound calls whose target comes from a user, a tenant setting or a webhook configuration: fetch(, axios, got, undici, requests.get, httpx, aiohttp. Each one is on the outbound path if the runtime or library underneath decodes Brotli. This is a code review, not a scan; a guide to supply-chain review covers how to keep that inventory current.

What should I patch first?

Upgrading the component that holds the decoder is the fix. Everything else reduces exposure while you roll it out.

You run Upgrade to Notes
Node.js 22, 24, 26 22.23.4, 24.22.1, 26.12.1 Rebuild every image and single-executable app; each carries its own runtime
Node.js 25 or older lines a supported line End-of-life lines get no upstream fix; 20.x ended on 30 April 2026
Python Brotli 1.2.1 pip install -U "Brotli>=1.2.1", then regenerate lockfiles and rebuild images
Python brotlicffi 1.2.1.0 pip install -U "brotlicffi>=1.2.1.0"
Bun 1.4.4
System libbrotli / curl your distribution's patched package or Brotli 1.0.10, 1.1.1, 1.2.1
Envoy, Istio see the Aikido list only if decompressor filters are enabled

Updating the npm package called brotli does nothing here: in Node.js the vulnerable code is the runtime's own copy.

Until the upgrade lands: refuse Brotli request bodies

If you cannot upgrade a service today, reject Brotli-encoded requests before any body parser touches them. In Express, register this before express.json() and the other parsers:

// Reject Brotli request bodies until the runtime is patched (CVE-2026-111497).
function rejectBrotliBodies(req, res, next) {
  const encoding = String(req.headers["content-encoding"] || "");
  if (/(^|,)\s*br\s*(,|$)/i.test(encoding)) {
    res.status(415).send("Brotli-encoded request bodies are not accepted");
    return;
  }
  next();
}

app.use(rejectBrotliBodies);
app.use(express.json());

At an Nginx reverse proxy in front of the app, the same rule:

if ($http_content_encoding ~* "(^|,)\s*br\s*(,|$)") {
    return 415;
}

This closes the inbound path only. It does nothing for outbound fetch() calls or for code that decompresses uploads; those need the upgrade. The Node.js advisory publishes no workaround for the runtime itself.

Key takeaways

  • Run node -p "process.versions.brotli" in every production image today; a version from 1.0.3 to 1.2.0 means the image needs a rebuild on Node.js 22.23.4, 24.22.1 or 26.12.1.
  • Check Python environments and images for Brotli and brotlicffi, not only your direct dependencies: HTTP clients load them automatically when present.
  • Find out whether your API decodes Brotli request bodies with one valid compressed request; a 415 means the inbound path is closed.
  • List the places your server fetches URLs it does not control; a proxy rule cannot protect those.
  • Treat end-of-life Node.js lines as unfixable for this CVE and plan the move to a supported line.

FAQ

Is HTTP2Shell a bug in HTTP/2?

No. The bug is in the Brotli decoder, and it is triggered by Brotli-compressed data regardless of the HTTP version that carried it. The name reflects that it reaches applications through ordinary HTTP request and response bodies.

Are Node2Shell and HTTP2Shell different vulnerabilities?

No. Both names refer to CVE-2026-111497. Node2Shell was the first name, when only Node.js had disclosed; it was renamed once Python packages, HTTP clients and proxies were found to carry the same decoder.

Is my Express 4 app affected?

Express 4 with body-parser 1.x does not decode Brotli request bodies, so the inbound path is closed. The app is still exposed if it calls fetch() on URLs it does not control or decompresses Brotli itself, because the runtime's decoder is still vulnerable until Node.js is upgraded.

Does updating the brotli package from npm fix it?

No. The npm package named brotli is a separate JavaScript library. Node.js uses its own bundled copy of the C decoder, which only a Node.js upgrade replaces.

Is it being exploited?

As of 24 October 2026, no exploitation in the wild had been reported and the CVE was not in CISA's Known Exploited Vulnerabilities catalog. Public proof-of-concept code exists, and code execution has been demonstrated on at least one official Node.js build, so the window for a calm upgrade is short.

Sources

Want to see which of your own endpoints take request bodies before you test them? Try it on your own site at hackergpt.app, or practise the checks on CyberGym.