← back to blog
Threats & Exploits · Topic 9

A Compression Library Mistook Bad Data for Send More, and Node.js Got an RCE

CVE-2026-111497 Node2Shell, a Brotli decoder out-of-bounds write in Node.js - Threats and Exploits

A decompressor has to answer one question about every byte it reads: is this data broken, or is there just more of it coming? CVE-2026-111497, nicknamed Node2Shell, is what happens when the code answers both with the same word. Bad data gets treated as "send me more", the decoder keeps feeding bytes into a buffer that holds eight, and the write runs off the end.

If you run Node.js, Bun, .NET, Java, Python or Envoy anywhere that touches Brotli, this is for you. It's in CISA's Known Exploited Vulnerabilities (KEV) catalogue, so I'd rather you spend ten minutes checking now than find out from a log later. By the end you'll know what the bug is, which of your services can actually reach it, and what to patch.

CVE-2026-111497 explained: the Brotli decoder returns one result for both malformed data and need more input, so a streaming decoder keeps writing past an eight byte buffer
One return value, two meanings. That ambiguity is the bug.

What happened

Brotli is a compression format from Google. Browsers ask for it with Accept-Encoding: br, and servers reply with Content-Encoding: br. Runtimes ship the C decoder inside themselves, which is why one bug in one library shows up in so many products.

Node.js released security updates on 21 October 2026, alongside fixed Brotli releases. According to Datadog's write-up, the fix had actually been committed to the upstream Brotli repository on 27 February 2026, which is eight months earlier. A public proof of concept appeared on 22 October. .NET followed with an out-of-band update on 25 October. The CVE record scores it CVSS 4.0 9.2 (Critical), while Node.js itself rates it High. I haven't read the vector string for either score, so I'm not going to quote one.

On exploitation: Datadog states there is confirmed in-the-wild exploitation, and a Viakoo news round-up lists CVE-2026-111497 in KEV from 27 October, also noting Siemens advisories for products that bundle the decoder. Who is exploiting it, and how many victims, isn't public in anything I read. I'd treat any article that names an actor for this one as unverified until a vendor or incident response firm says so.

Eight months in a public repo

From upstream commit to KEV
01
27 Feb

Fix committed to the upstream Brotli repository.

▸
02
21 Oct

Node.js security releases and fixed Brotli versions ship.

▸
03
22 Oct

Public proof of concept appears. It crashes and leaks a little memory.

▸
04
25 Oct

.NET out-of-band update: 8.0.33, 9.0.22, 10.0.14.

▸
05
27 Oct

Listed in CISA KEV, per Viakoo's round-up of the catalogue.

Dates from Datadog Security Labs and Viakoo. The discoverer isn't named in the pages I read.

My opinion, and it's only an opinion: the dates are the real story here. A fix sitting in a public repository for months, with no release attached, gives anyone who reads commits a head start over everyone who only reads advisories. I'm not saying that's what happened in this case. I'm saying it's the kind of gap defenders can't see, and that's worth knowing when you decide how fast to patch.

The root cause: one boolean, two meanings

Datadog traces it to a function called DecodeBlockTypeAndLength() in the Brotli decoder. It returns a boolean. Callers read false as "I need more input", but the same false is also used for "this stream is malformed". They can't tell the two apart. On the decoder's fast path, the return value is dropped completely.

Akamai describes the trigger. A stream can signal a block switch that its own structure makes invalid. The decoder should reject that with a permanent format error. Instead it reports a temporary need for more input, so the streaming decoder keeps appending incoming bytes to an eight-byte internal buffer. It never checks the bound, and the write goes past the end. In Datadog's description the overflow spills into adjacent decoder state inside a single heap allocation.

Think of a form with a "page incomplete, please continue" box. If a clerk ticks that box for forms that are actually forged, the forger just keeps handing over more pages. Nothing ever says stop.

The upstream fix gives invalid block switches their own error, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, so the stream is rejected instead of waiting for more. There's also a second path in the same area that retries a forbidden block switch in a loop. That one pins a CPU core, which means a single request can cause denial of service even if nobody ever gets code execution.

Weakness class: out-of-bounds write (CWE-787). In ATT&CK terms the entry point is T1190, Exploit Public-Facing Application, when the decoder sits behind a server that takes request bodies.

How far from a crash to a shell?

I'll be careful here, because the sources disagree on how finished the exploit is. Datadog says the corrupted state controls where the decoder reads and writes next, which gives two building blocks: a repeatable heap read that comes back out through the decompressed response, and a redirected write. In their lab the write hit an unmapped page and crashed the process every time. They list three obstacles to a reliable shell: heap addresses that change on every request, the fact that the corruption bytes are also decoder input, and the managed runtime's allocator and defences. They call the gap engineering effort, and they did not produce a full shell.

Imperva reports getting code execution on the official Node.js 24.22.0 Linux ARM64 image after several attempts, and says it hasn't released the exploit. I haven't reproduced any of this and I won't be publishing exploit steps. The practical reading is that a crash and a memory leak are easy today, and remote code execution has been shown by at least one vendor. Plan for the worst of those.

Which of your services can actually reach it

Per Akamai, a bare Node.js HTTP server does not decode incoming Brotli bodies on its own. You need something that decompresses untrusted data. The paths they list:

  • zlib.brotliDecompress() or zlib.createBrotliDecompress() given input from outside.
  • The built-in fetch(), which decompresses Brotli responses. Here a hostile or compromised server attacks your client, so the exposure is outbound, not inbound.
  • Express 5 and body-parser 2 parsers (json, raw, text, urlencoded). Their inflate option defaults to true, so a request with Content-Encoding: br is decoded for you.

That last one is the dangerous default. Plenty of teams run an API on Express 5 and have never typed the word Brotli.

Affected and fixed versions

ProductAffectedFixed
Node.js 22, 24, 26Before 22.23.4, 24.22.1, 26.12.122.23.4, 24.22.1, 26.12.1
Google Brotli1.0.3 to 1.0.9, 1.1.0, 1.2.01.0.10, 1.1.1, 1.2.1
Python Brotli / brotlicffi1.2.0 and earlier1.2.1 / 1.2.1.0
Bun1.0.21 to 1.4.31.4.4
Java brotli4j1.23.0 and earlier1.24.0
.NET 8, 9, 10Before the 25 October 2026 update8.0.33, 9.0.22, 10.0.14
EnvoyBefore the first fix on each branch1.36.12, 1.37.8, 1.38.6, 1.39.3, 1.40.0
IstioBefore the first fix on each branch1.29.9, 1.30.6, 1.31.2

End-of-life Node.js lines (25.x, 23.x, 20.x and older) carry the vulnerable decoder and get no patch. If you're on one, the answer is to upgrade the major version, not to wait.

How to check your own estate

Start with the Node.js version on every host and in every container image:

node -v
node -e "console.log(process.versions.node)"

Compare the result with the table above. One trap to know about: Datadog notes that process.versions.brotli still reports 1.2.0 on the patched Node.js 24.22.1. So don't use it to decide whether you're fixed. Trust the Node.js version number.

Then search your code for the exposed paths:

grep -rn "brotliDecompress\|createBrotliDecompress" --include=*.js --include=*.ts src/
grep -rn "body-parser\|express.json\|express.urlencoded" --include=*.js --include=*.ts src/

Anything that uses Express 5 or body-parser 2 without inflate: false should be treated as exposed. Datadog adds that Java brotli4j bundles its own native build, so patching the operating system's libbrotli doesn't fix a Java service. Rebuild container images and Bun compiled executables as well, because the old decoder is baked in.

Common mistakes when patching this

First, patching the package but not restarting. Datadog says every process has to be restarted, or the old library stays in memory. Second, trusting the interim measures. Setting inflate: false on Express 5 and body-parser 2, or rejecting Content-Encoding: br at the reverse proxy, only closes the inbound request path. It can also break legitimate compressed clients. It does nothing for fetch(), axios, got, node-fetch or Python HTTP clients that decode Brotli responses automatically. Third, assuming a service with no public inbound route is safe. If it calls out to anything you don't control, it can still be hit through the response.

Patch is the only complete fix Node.js offers no workaround. Block-and-restrict options reduce the inbound path only. Use them to buy a few hours, then upgrade.

Detecting it

There's no clean log signature in what I read, only behaviour. Datadog suggests watching for:

  • Decoder errors that differ between patched and vulnerable builds.
  • Worker processes exiting with SIGSEGV (exit code 139) or sitting at 100 percent CPU.
  • Unexpected decompressed output.
  • Shells or new outbound connections from web workloads.

A web worker that crashes with 139 right after a request carrying Content-Encoding: br is worth an hour of your time, even if it turns out to be a bad client.

Quick recap

  • CVE-2026-111497 (Node2Shell) is an out-of-bounds write in the Brotli decoder, scored CVSS 4.0 9.2, in CISA KEV.
  • The decoder returned one value for "malformed" and "need more input", so invalid streams kept writing into an eight-byte buffer.
  • Reachable through zlib.brotliDecompress, outbound fetch(), and Express 5 or body-parser 2 request bodies by default.
  • Fixed in Node.js 22.23.4, 24.22.1 and 26.12.1. Other runtimes have their own fixed versions, and end-of-life Node lines get none.
  • Patch, rebuild images, restart everything. Proxy blocks cover only inbound requests.

Check this today

Run node -v across your hosts and images, then grep your services for Express 5 without inflate: false. Write down every service that makes outbound calls to third parties, because those need the patch even if nothing points at them from the internet.

If you're testing this in your own lab, or you've found a fleet with a pattern I haven't covered, I'd be glad to compare notes on LinkedIn.

References

FAQ

What is CVE-2026-111497?

CVE-2026-111497, nicknamed Node2Shell, is an out-of-bounds write in the Google Brotli decoder that Node.js and several other runtimes bundle. Malformed compressed data can overflow an internal buffer. The CVE record scores it CVSS 4.0 9.2. It is in CISA's Known Exploited Vulnerabilities catalogue, and fixed Node.js releases shipped on 21 October 2026.

Is my Node.js server exposed if it never uses Brotli?

A bare Node.js HTTP server does not decode incoming Brotli bodies on its own. You are exposed when your code decompresses untrusted data, for example with zlib.brotliDecompress, with fetch() reading a hostile server's response, or with Express 5 and body-parser 2, which decode Content-Encoding br request bodies by default.

Why does process.versions.brotli still say 1.2.0 after patching?

Datadog notes that process.versions.brotli still reports 1.2.0 on the patched Node.js 24.22.1. So that value cannot tell you whether you are fixed. Check the Node.js version itself instead: 22.23.4, 24.22.1 or 26.12.1 and later contain the fix, and end-of-life lines have no patch.

Can I block it at the reverse proxy instead of patching?

Only partly. Rejecting Content-Encoding br requests, or setting inflate to false in Express 5 and body-parser 2, closes the inbound request path. It does not cover outbound calls, where fetch(), axios and Python clients decode Brotli responses automatically. Patching and restarting every process is the only complete fix.