Back

26/10/2026

HTTP2Shell (CVE-2026-111497): updating a binary does not patch running processes

The Brotli flaw behind HTTP2Shell reaches Node.js, Bun and Python. How to find real exposure in live processes and restart AI agents safely.

On 21 October 2026 Node.js shipped fixes for CVE-2026-111497, an out-of-bounds write in Google’s Brotli decoder. Advisories for Bun, the Python brotli and brotlicffi packages, Envoy and Istio followed within days, and the security press named the flaw Node2Shell and later HTTP2Shell. Despite the nickname, it is not a bug in the HTTP/2 protocol. It is a decompression bug, and it matters wherever software decodes Brotli data that an attacker can influence.

This article sets out what is verified, what is only possible, and how to check your own systems. It also covers a lesson from our own response: installing a fixed binary does not fix a process that is still running the old one. In our environment, long-running Claude Code sessions kept their old embedded runtimes after the installed Claude Code had been updated.

Key takeaways

  • Affected: upstream Brotli 1.0.3 to 1.2.0. Fixed: 1.0.10, 1.1.1 and 1.2.1. Products that bundle Brotli need their own fixed release.
  • Exposure needs a real path: attacker-influenced Brotli data reaching a vulnerable decoder. A product name or network access alone is not proof.
  • Check live processes, not just installed packages. A process keeps the runtime it started with until it is restarted.
  • Restart stateful AI agents one at a time, with their events, approvals and work state handed over first.

1. What the vulnerability is

When the decoder met an invalid block switch, it reported “need more input” instead of a permanent format error. A streaming caller then kept feeding bytes, and the decoder appended them to an eight-byte internal buffer, writing past its end. The Node.js advisory says an attacker who can supply Brotli data that an application decompresses with node:zlib “can corrupt process memory, which can lead to a crash, disclosure of process memory, or remote code execution”.

Severity ratings differ by source. Node.js rates it High. The GitHub advisory for the Python packages and the Envoy advisory rate it Critical, CVSS 4.0 score 9.2. Ubuntu currently lists it as Medium priority. As of 26 October, CISA’s assessment on the CVE record lists exploitation as “none”, and the CVE is not in CISA’s Known Exploited Vulnerabilities catalogue. The Node.js advisory still names remote code execution as a possible outcome, so do not treat the absence of reported attacks as a reason to wait.

2. Prerequisites: when you are actually exposed

All three conditions must hold:

  1. A vulnerable decoder is loaded in the process: a bundled or system Brotli library in the affected range without a fix or backport.
  2. Something calls it: explicit decompression, or automatic decoding of Content-Encoding: br.
  3. The input can be attacker-influenced: responses from arbitrary URLs, uploaded files, incoming request bodies, or plain-HTTP traffic on a hostile network.

Automatic decoding is the path teams miss most often. Bun’s advisory notes that fetch sends Accept-Encoding: br by default and decodes the response, so any Bun program that fetches untrusted URLs has a path. In Python, once brotli or brotlicffi is installed, urllib3 automatically decodes Content-Encoding: br responses, and requests also advertises br in its requests. This happens even when your own code never mentions Brotli. Envoy and Istio are only exposed when a Brotli decompressor is configured.

3. Confirmed affected versus merely possible

Status as of 26 October 2026. Check the linked advisories, because this is moving quickly.

ComponentStatusFixed in
Upstream Brotli (C)Confirmed: 1.0.3 to 1.2.01.0.10, 1.1.1, 1.2.1
Node.js 22, 24, 26Confirmed22.23.4, 24.22.1, 26.12.1
Node.js 20 and older (end of life)Not assessed; Node.js treats end-of-life lines as always affectedMove to a supported line
Bun (including compiled executables)Confirmed: 1.0.21 to 1.4.31.4.4, and rebuild compiled apps
Python brotli / brotlicffiConfirmed: from 1.0.31.2.1 / 1.2.1.0
Envoy (Brotli decompressor configured)Confirmed1.40.0, 1.39.3, 1.38.6, 1.37.8 (1.36.12 on the unsupported 1.36 branch)
Istio (Brotli decompression enabled)Confirmed1.31.2, 1.30.6, 1.29.9 (the final 1.29 release)
Ubuntu brotli packageNeeds evaluation (all releases)No fixed package yet
Ubuntu nodejs packageVulnerable on 20.04 (bundled Brotli). On 22.04 and later it uses the system Brotli, so it follows the row aboveSee Ubuntu tracker
Debian brotli packageFixed in bookworm and trixie; forky not yet1.0.9-2+deb12u1, 1.1.0-2+deb13u1
Chrome, Electron and Electron appsUnknown: no vendor statement foundWatch vendor release notes

On the last row: Chrome and Electron ship their own Brotli copies, and an Electron app also embeds a Node.js runtime. At the time of writing, the newest Electron stable releases (45.0.0, 44.9.3 and 43.8.3, all 21 October) list Node.js 24.22.0, not 24.22.1, and their notes do not mention this CVE. Electron pre-releases from 23 October carry Node.js 24.22.1, so a stable fix looks likely, but it has not shipped yet. That makes a desktop app built on Electron possibly exposed, not confirmed vulnerable. The same applies to chat clients and AI desktop tools: check each vendor’s own statement before calling them affected or safe.

One more trap: a reported library version is not proof either way. A fixed Node.js 22.23.4 still reports process.versions.brotli as 1.1.0, because the fix was backported. Distributions do the same. Judge by the product’s fixed version or the distribution’s security status, not by the Brotli version string.

4. What we found in our own environment

We ran a read-only inventory before changing anything. Here is what we observed, anonymised, with confirmed findings kept separate from risk we could not verify.

Confirmed observations

  • The installed Claude Code had already been updated, but a dozen long-running sessions were still running older builds started weeks earlier. They had never been restarted.
  • Our first count missed three of them. They ran under a different helper process, so a search by command line did not match. Searching by executable found them.
  • The old builds’ files had already been deleted from disk by the updater, so the embedded Bun version of those processes could not be read.
  • A leftover package-manager install of an older Claude Code build embedded Bun 1.4.0. Nothing used it in normal operation, but a fallback path could have. We removed it once we had shown nothing depended on it.
  • An unused end-of-life Node.js runtime was still installed on a server. We removed it the same way.
  • The current Claude Code build we upgraded to (2.1.310) contains Bun 1.4.4. We confirmed this by inspecting the binary. Anthropic’s changelog does not mention the CVE, so check your own build the same way.

Not verified, treated as risk

  • Whether the old sessions were in fact vulnerable. Their Bun version was unreadable. The fixed Bun 1.4.4 only appeared around 22 October, after those builds, so we treated them as affected.
  • Whether an attacker could have reached the decoder through those sessions, for example by fetching a malicious URL. This path is plausible from Bun’s documented fetch behaviour, but we did not prove it.
  • Whether other software that bundles its own Brotli copy is affected. Where no vendor statement exists, we track the product against the vendor’s releases rather than assume it is safe or vulnerable.

We found no evidence of exploitation. We also did not treat that as proof of safety: the fix was to remove the exposure, not to argue it away.

5. Detection and remediation checklist

Find the dependencies

Look for Brotli where it is bundled, not only where it is installed as a package. Check your SBOMs and container images for Node.js, Bun, Python packages, Envoy and statically linked Brotli. Check transitive Python dependencies, because Brotli often arrives as an optional extra.

# Node.js: compare with 22.23.4 / 24.22.1 / 26.12.1
node -p process.version

# Python packages: run once per interpreter and virtual environment
python -m pip list 2>/dev/null | grep -iE '^(brotli|brotlicffi) '
pipdeptree -r -p brotli          # what pulled it in

# Debian / Ubuntu system library (then check the distro tracker)
dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' 'libbrotli*'

# Container images, with an SBOM scanner such as Syft
syft <image> | grep -iE 'brotli|node|bun'

Find the embedded runtimes

Programs built with bun build --compile carry their own copy of Bun. Bun’s advisory says they stay affected until they are rebuilt with 1.4.4. Upgrading Bun on the build machine does not change binaries that are already deployed.

# The Bun version inside a compiled executable
grep -aoE 'Bun v1\.[0-9]+\.[0-9]+' /path/to/binary | sort -u

Find the live processes

Check what is running, not what is installed. On Linux, a process whose executable was replaced or removed shows (deleted). That is a strong sign it is still running an old build.

# Processes still running a deleted (replaced) executable (run as root to see all users)
for p in /proc/[0-9]*; do
  e=$(readlink "$p/exe" 2>/dev/null) || continue
  case "$e" in *"(deleted)") echo "${p#/proc/} $e";; esac
done

# Processes older than the fix
pgrep -af 'node|bun|claude'      # then: ps -o pid,lstart,args -p <pid>

On macOS, lsof -p <pid> shows the executable a process loaded. Match by executable, not by process name, or you will miss helpers and background workers.

Fix, rebuild, redeploy

  • Upgrade to the fixed versions in the table. Use your distribution’s patched package where a backport exists.
  • Rebuild container images and compiled executables, then redeploy them. A patched base image does nothing for an image you have not rebuilt.
  • Remove unused runtimes and leftover installs, after proving that no service, job or script depends on them. Keep a rollback.
  • Where no fix exists yet, disable Brotli decoding of untrusted input where it is safe to do so. For Bun, fetch(url, { decompress: false }) works per request. Give each mitigation an owner and an end date.

Restart and verify

After restarting, check the new process’s actual executable and runtime version. Then check the business output: queues drained, scheduled jobs succeeded, monitoring alerts arriving. A successful upgrade command proves only that the command ran.

6. Restarting persistent AI agents safely

Long-running AI agents hold state: pending tool calls, approvals, queued messages and claims on work. Restarting them all at once can lose that state. This sequence worked for us:

  1. Inventory each session: executable version, state (idle, waiting or busy), last activity, pending work, approvals it is waiting for or holds, unacknowledged events and how it will be recovered.
  2. Move approvals out of memory. An approval that lives only in a session’s conversation is lost on restart. Before restarting, record each pending approval (what was approved, by whom, for which exact change) somewhere durable, such as the ticket or pull request. Afterwards, confirm the session still sees it. If in doubt, ask for the approval again rather than assume it.
  3. Prove the replacement first. Confirm that sessions on the fixed build already work, and that an event watcher independent of the old sessions is running, so no event depends on a session being restarted.
  4. Restart idle sessions first, one at a time. After each one, verify the old process is gone, the new process runs the fixed executable, the conversation history is intact and any remote connection is back. Then do the next one.
  5. Restart the busiest owner last, between tasks. Have it write a handoff note, stop its watcher only at zero pending and zero unacknowledged events, restart, then confirm its watcher resumed from the saved position with nothing missed.
  6. Expect queued input on reconnect. One of our restarted sessions received a message that had been waiting for ten days and acted on it. Check each session’s inbox before restarting, and tell its owner.
  7. Stop and escalate if any step cannot show that work was recovered. Do not fall back to killing everything.

Define “done” before you start. Done means no running process on an affected build, every exception documented with an owner and a recheck date, and the business checks passing. “The package was upgraded” is not done.

Sources