Skip to main content

Node2Shell: when decompression corrupts the process

How CVE-2026-111497 turns a Brotli decoder error into memory corruption, which Node.js applications are exposed, and how to patch them.

The internal buffer involved in CVE-2026-111497 was eight bytes long. This is the Brotli decoder vulnerability ELLIO calls Node2Shell. The Node.js advisory describes a decoder that kept appending input after it should have rejected the stream. It eventually wrote beyond that buffer.

Node.js shipped fixes on October 21, 2026 in versions 26.12.1, 24.22.1 and 22.23.4. Exposure requires attacker-controlled Brotli bytes to reach a vulnerable decoder through node:zlib, directly or through a dependency. Tracing those bytes through the decoder explains the memory corruption and the checks needed for a deployed service.

Decompression happens before application validation

The tempting assumption is that a JSON endpoint only needs to defend its JSON parser. But a compressed request must first become JSON bytes. If middleware decompresses the body before the handler runs, schema validation in that handler happens after the native decoder has already processed the attacker’s input.

The Node.js 24.22.1 zlib documentation exposes Brotli decoding through brotliDecompress(), brotliDecompressSync() and createBrotliDecompress(). These APIs run the native decoder inside the application’s process.

Inspect the code that decompresses input:

  • Check request-body middleware. body-parser 2.4.0 documents automatic Brotli inflation for its parsers, with inflate enabled by default.
  • Check outbound HTTP clients. The Undici implementation bundled with Node.js 24.22.1 selects zlib.createBrotliDecompress() for responses with Content-Encoding: br. Check clients as well as servers, especially URL previewers and fetch jobs that contact user-selected destinations.
  • Check direct decoding in upload handlers, import jobs and queue consumers. Trace the bytes passed to each Brotli decompression call back to their producer. A file or queue message can carry untrusted compressed data without an HTTP content-encoding header.

For each path, identify who controls the compressed input that the process decompresses.

An invalid state became a request for bytes

A streaming decoder cannot assume that every call contains a complete stream. It needs to distinguish a valid prefix that requires more bytes from an invalid encoding that no additional bytes can repair. That distinction controls whether the caller should resume decoding or stop.

RFC 7932’s block-switch encoding specifies how the decoder selects the next block type and length. The upstream Brotli fix shows where error handling broke. Before the change, DecodeBlockTypeAndLength() returned a Boolean. An invalid block switch and an incomplete read could both return false, and some block-switch wrappers discarded the result.

According to the Node.js advisory, the streaming decoder treated the invalid switch as a temporary need for input. It continued collecting bytes in its eight-byte internal buffer instead of reporting a permanent format error. More input could not make the invalid state valid, but the decoder kept accepting it.

These selected hunks from the public fix show the return-type change and the two cases that previously returned false:

google/brotli 0ed4268: c/dec/decode.c (excerpts)
@@ -1176,7 +1176,7 @@ static BrotliDecoderErrorCode DecodeContextMap(brotli_reg_t context_map_size,
/* Decodes a command or literal and updates block type ring-buffer.
Reads 3..54 bits. */
-static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
+static BROTLI_INLINE BrotliDecoderErrorCode DecodeBlockTypeAndLength(
int safe, BrotliDecoderState* s, int tree_type) {
brotli_reg_t max_block_type = s->num_block_types[tree_type];
const HuffmanCode* type_tree = &s->block_type_trees[
@@ -1187,7 +1187,7 @@ static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
brotli_reg_t* ringbuffer = &s->block_type_rb[tree_type * 2];
brotli_reg_t block_type;
if (max_block_type <= 1) {
- return BROTLI_FALSE;
+ return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
}
/* Read 0..15 + 3..39 bits. */
@@ -1197,11 +1197,13 @@ static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
} else {
BrotliBitReaderState memento;
BrotliBitReaderSaveState(br, &memento);
- if (!SafeReadSymbol(type_tree, br, &block_type)) return BROTLI_FALSE;
+ if (!SafeReadSymbol(type_tree, br, &block_type)) {
+ return BROTLI_DECODER_NEEDS_MORE_INPUT;
+ }
if (!SafeReadBlockLength(s, &s->block_length[tree_type], len_tree, br)) {
s->substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_NONE;
BrotliBitReaderRestoreState(br, &memento);
- return BROTLI_FALSE;
+ return BROTLI_DECODER_NEEDS_MORE_INPUT;
}
}

The invalid switch now returns BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. An incomplete read returns BROTLI_DECODER_NEEDS_MORE_INPUT. The rest of the diff handles these statuses through the block-switch paths in ProcessCommandsInternal. Distinguishing and passing on these statuses lets the decoder stop on malformed input.

The Node.js 24.22.1 release notes also list the newly exposed constant BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. It identifies the format error for a rejected block switch.

Corrupted decoder state can crash, leak or execute

An out-of-bounds write stores bytes outside the memory region intended for them. In this case, the CVE record maintained by Google as the assigning authority identifies corruption of adjacent decoder state memory. A parser has turned attacker-controlled input into unintended changes to the process’s own state.

The advisory and CVE record list crashes, memory disclosure and possible code execution. One way to reason about those outcomes is to follow what later operations do with the corrupted state. An invalid address can cause a process crash. A subsequent read or copy using corrupted state can disclose process contents. Redirected execution can allow attacker-chosen code to run.

A JavaScript try/catch cannot restore memory already overwritten by native code. The memory corruption occurs before application-level error handling can act.

ELLIO reports getting a shell on an official, unmodified Node.js 24.22.0 build. It reproduced the result on x86-64 against four targets with address space layout randomization enabled. Its October 26 update reports exploit traffic observed by its sensors. CISA added the CVE to its Known Exploited Vulnerabilities catalog on October 27, citing evidence of active exploitation.

Node.js rates the issue High, while Google’s CNA assessment gives it CVSS 4.0 9.2, Critical. Scanner severity can therefore differ according to which assessment it uses.

Check the runtime that actually serves traffic

The release notes name the first patched official release in each affected supported line:

Node.js lineFirst patched release
26.x26.12.1
24.x24.22.1
22.x22.23.4

For official binaries on these lines, an earlier release needs an update. Use the latest supported patch release in the chosen line. For distribution packages or custom builds, check the distributor’s advisory because fixes can be backported.

Distribution builds may link a shared libbrotli, as ELLIO explains. The Debian tracker records that “nodejs and python-brotlicffi use system provided brotli library”. On these builds, update the system Brotli library and restart dependent processes. Updating Node.js alone leaves the vulnerable library in place.

Check the distribution’s brotli package against the CVE’s fixed upstream versions, 1.0.10, 1.1.1 and 1.2.1, or a documented distribution security backport.

Check node --version inside each deployed container or service environment, not only on a developer’s laptop. Where the application reports its runtime, Node’s process.version property identifies the running Node.js version. Inventory background workers and scheduled jobs too. Then rebuild and restart deployments so existing processes no longer run the old binary.

For builds that bundle Brotli, the Node.js release notes explicitly list a cherry-picked change. A library version string alone can miss a backported security fix, so verify the runtime release and its patch provenance. Replace or rebuild the executable to update its embedded decoder.

For separately managed Brotli libraries, the CVE record gives three affected ranges: 1.0.3 through 1.0.9, 1.1.0, and 1.2.0. It names 1.0.10, 1.1.1 and 1.2.1 as the respective fixes. Those are Brotli versions, not Node.js versions.

The Node.js advisory states: “End-of-Life versions are always affected when a security release occurs.” Move end-of-life installations to a supported line.

Remove decoding paths while patching

Updating the affected decoder is the fix, whether it comes with Node.js or a system library package. Until rollout completes, prevent untrusted Brotli data from reaching it. Apply controls before decompression, wherever that first happens.

For inbound requests, reject unsupported content encodings before body-parsing middleware runs. If compressed requests are unnecessary, body-parser documents inflate: false as rejecting compressed bodies. Verify the deployed middleware version and ordering, including routes that use a different parser.

For outbound clients, restrict attacker-selected destinations or suspend the feature that fetches untrusted content. The bundled Undici response-decoding path selects Brotli whenever the response’s Content-Encoding says br, independently of the request’s Accept-Encoding. Removing br from Accept-Encoding is therefore not a mitigation.

Request limits and timeouts remain useful resource controls. The zlib documentation’s maxOutputLength option limits decompressed output for convenience methods. It is not a fix for an internal decoder-state overwrite. Running with fewer privileges and isolating decompression in a separate process can reduce the consequences of memory corruption.

Can someone outside the trust boundary choose bytes that this process sends to Brotli decompression? If yes, prioritize the decoder update and block that path until the update is deployed. The eight-byte buffer is internal, but the input that reaches it may come from a request handler, an HTTP client or a background job.