Cyberc0re
1598 words
8 minutes
Node2Shell - CVE-2026-111497

Overview#

Node2Shell (CVE-2026-111497) is a memory-corruption vulnerability in Google’s native C Brotli decoder, disclosed by Node.js on October 21, 2026. A malformed Brotli stream triggers incorrect error handling, allowing continued processing and an out-of-bounds write. Attackers can exploit it during decompression, before an API’s route-level input validation runs.

1. Vulnerability Profile#

PropertyDetail
IdentifierCVE-2026-111497
WeaknessCWE-787 - Out-of-bounds Write
ComponentGoogle Brotli C decoder, c/dec/decode.c
Node.js severityHigh (CVSS v4.0: 9.2, Critical, in the CVE record)
Required exposureUntrusted Brotli input reaches a vulnerable decoder
First fixed Node.js versions22.23.4, 24.22.1, 26.12.1

Brotli is a compressed-data format, not an application protocol. The dangerous boundary is decompression, whether the compressed bytes arrive in a request body, an upstream HTTP response, or another input processed through node:zlib.

2. HTTP Entry Point: Express Body Parsing Before Route Execution#

In Express 5, the JSON parser can inflate a Brotli request body when it receives Content-Encoding: br. The route below is the application-level entry point:

const express = require('express');
const app = express();

app.post('/upload', express.json(), (_req, res) => {
  res.status(200).type('text/plain').send('DONE\n');
});

app.listen(3000, '127.0.0.1');

The important call is express.json(), not the string returned by the handler. Its decompression step precedes JSON parsing and the route callback. A simple Node.js http.createServer() does not automatically perform this decompression.

2.1 Brotli request body#

Here is a request that illustrates the byte-level HTTP framing:

POST /upload HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Content-Encoding: br
Content-Length: 18
Connection: close

The body after the blank line is binary, not the literal text {"probe":"ok"}. Its exact 18 bytes are:

Offset    Bytes
00000000  8b 06 80 7b 22 70 72 6f 62 65 22 3a 22 6f 6b 22
00000010  7d 03

This Node.js snippet independently checks the byte length and decompressed JSON:

const { brotliDecompressSync } = require('node:zlib');

const encoded = Buffer.from(
  '8b06807b2270726f6265223a226f6b227d03',
  'hex'
);

console.log(encoded.length);                         // 18
console.log(brotliDecompressSync(encoded).toString()); // {"probe":"ok"}

The request enters the native Brotli decompression path before JSON parsing or route execution. When malformed compressed data triggers an invalid block-type transition, the vulnerable decoder incorrectly handles the failure as an insufficient-input condition, allowing processing to continue into memory corruption.. Its length and content cannot be inferred from this 18-byte request.

Node2Shell request and decoder flowA Brotli-encoded upload is decompressed before JSON parsing. Valid input reaches the route. An impossible block switch continues processing on a vulnerable decoder, but is rejected by a patched decoder.REQUESTPOST /uploadContent-Encoding: brEXPRESS 5express.json()Inflates before JSON parsingNATIVE DECODERBrotli · node:zlibBlock-type transitionVALID STREAMNormal decodeJSON parsesRoute handler runsVULNERABLEInvalid block switchMisread as “need more input”Processing continuesPATCHEDInvalid block switchFORMAT_BLOCK_SWITCHRejected; route is not run

3. Root Cause: A Format Error Was Returned as a Retry Condition#

The source code level condition is in DecodeBlockTypeAndLength() inside c/dec/decode.c. The decoder tracks block lengths and block-type counts in BrotliDecoderState. When the current command block length reaches zero, decoding may require a block switch. However, requesting a switch when the block-type count is at most one is invalid.

The vulnerable logic effectively made the following decision:

/* Affected behavior — condensed from the upstream diff */
if (max_block_type <= 1) {
    return BROTLI_FALSE;
}

BROTLI_FALSE was also used for an ordinary streaming condition: the decoder does not yet have enough input bits. That overload is the bug. Invalid input is a permanent condition, insufficient input is potentially recoverable.

The fix changes the function’s return type to BrotliDecoderErrorCode so those states have distinct values:

/* Fixed behavior — decisive branch */
if (max_block_type <= 1) {
    return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
}

The caller also has to propagate the code rather than treating every non-success result as a retry. The patched logic is equivalent to:

/* Condensed illustration of status propagation */
BrotliDecoderErrorCode status =
    DecodeBlockTypeAndLength(safe, s, 1);  /* command block type */

if (status != BROTLI_DECODER_SUCCESS) {
    return status;  /* preserve FORMAT_BLOCK_SWITCH vs NEEDS_MORE_INPUT */
}

The actual upstream patch also introduces BROTLI_SAFE_WITH_STATUS for block-switch operations. This matters because the old BROTLI_SAFE path was designed around a boolean result, and in some fast-path execution it could ignore an error return. A distinct error code is only useful if it survives each calling layer.

3.1 Why this leads to memory corruption#

The broken transition is more specific than “the parser mishandles a bad file”:

  1. A command, literal, or distance block reaches a state that requires a block-switch decision.
  2. The compressed stream requests a switch even though the corresponding num_block_types value does not allow one.
  3. The vulnerable helper returns BROTLI_FALSE, which is interpreted as insufficient input instead of a terminal format error.
  4. Streaming processing continues, including input buffering. The Node.js advisory identifies an eight-byte internal decoder buffer that can be written past its end.
  5. The overflow can alter adjacent decoder state. Subsequent reads, writes, or control-flow decisions may then operate on corrupted values.

This is a C memory-safety bug caused by incorrect state-machine error propagation. It is not a JSON parser bypass, a command-injection sink, or an unsafe JavaScript function.

4. Exploitability: From Decoder State Corruption to Code Execution#

The out-of-bounds write is the initial primitive, not the end of the analysis. Corrupted decoder state can disturb later memory accesses; whether that becomes a crash, memory disclosure, or execution control depends on the target binary, heap/process layout, and the attacker’s ability to influence the corrupted region.

A published Linux x86-64 Express configuration used these concrete versions:

Node.js       24.22.0
Express        5.3.0
body-parser    2.4.0
Architecture   Linux x86-64
HTTP endpoint  POST /upload
Encoding       Content-Encoding: br

In that configuration, execution of the system id utility was demonstrated, and its output was observed on the HTTP connection:

uid=1000(node) gid=1000(node) groups=1000(node)

The uid=1000(node) and gid=1000(node) values show that the process executed under the Node.js service account. The security impact is native command execution from an HTTP-reachable decompression path, rather than an ordinary request-parsing failure or worker crash.

This behavior depended on the exact Node.js build, process layout, and worker configuration. Address-space layout randomization makes the result probabilistic, so code execution in this configuration does not establish reliable RCE against every Express endpoint that accepts br.

4.1 What changed in the patched decoder?#

When the same malformed stream was submitted to the fixed Node.js 24.22.1 configuration, the recorded response was a rejection rather than command execution:

HTTP/1.1 400 Bad Request

ERR ERR__ERROR_FORMAT_BLOCK_SWITCH

The ERR ... line comes from that application’s HTTP error handler, while BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH is the underlying native decoder error. They are related but are not the same string or abstraction level. The patched service also remained responsive to its health endpoint.

This affected-versus-fixed comparison is the strongest way to understand the security fix: an impossible block switch becomes a terminal error and is propagated through the decoder, preventing the invalid retry path.

5. Assessment and Remediation#

The first question is whether attacker-controlled bytes can reach native Brotli decompression. Check both request-body middleware and outbound HTTP clients. A Node.js application that fetches and consumes attacker-controlled Brotli responses can be exposed even if it has no compressed upload endpoint.

A useful runtime inventory command is:

node -p 'JSON.stringify({node:process.version, platform:process.platform, arch:process.arch, brotli:process.versions.brotli})'

The embedded Brotli version by itself is not a definitive patch indicator: Node.js or downstream distributors may backport fixes. Verify the actual deployed runtime and vendor advisory.

If you must temporarily disable inbound Brotli request-body inflation in Express:

app.use(express.json({
  inflate: false,
  limit: '16kb'
}));

That mitigation changes how your API handles compressed clients and does nothing for outbound response decompression. Patching the Node.js runtime is the durable fix. The first official fixed builds are 22.23.4, 24.22.1, and 26.12.1.

6. Conclusion#

Node2Shell is a good example of a vulnerability whose visible trigger is an HTTP request but whose root cause is much deeper in native code. The significant failure is that an impossible Brotli block switch was treated like a temporary shortage of input. Once that distinction was lost, the streaming decoder could continue into memory corruption.

The practical security boundary is straightforward: identify where untrusted compressed bytes reach the decoder, then ensure that decoder is patched. Correct JSON validation alone does not protect the native decompression path that runs before it.

References#

  1. Node.js security advisory - October 21, 2026
  2. Google Brotli upstream patch - commit 0ed4268
  3. Express 5 documentation
  4. Datadog Security Labs - Node2Shell analysis
  5. Express x86-64 demonstration and evidence
Node2Shell - CVE-2026-111497
https://cyberc0re.in/posts/node2shell-cve-2026-111497-analysis/
Author
Cyberc0re
Published at
2026-11-01