Threat intelligence, Threat Research

Node2Shell: Node.js Brotli Decoder Out-of-Bounds Write Remote Code Execution

by Security News

Node2Shell: Node.js Brotli
Pre-Auth RCE (CVE-2026-111497)

OVERVIEW

SonicWall Capture Labs threat research team became aware of the threat CVE-2026-111497, assessed its impact, and developed mitigation measures. The flaw, which circulates as Node2Shell, Broccoli2Shell, and HTTP2Shell, is a critical out-of-bounds write in the Google Brotli decoder that Node.js bundles to implement the br content coding. It is reachable by any application that decompresses an attacker-supplied Content-Encoding: br HTTP body, so a single unauthenticated request carrying a crafted Brotli stream drives the decoder past an eight-byte buffer and corrupts its state, a condition that public exploitation turns into remote code execution. Classified under CWE-252 (Unchecked Return Value) leading to CWE-787 (Out-of-bounds Write), it is rated CVSS 9.8 (Critical) by NVD, with Google, the coordinating CNA, scoring it 9.2 under CVSS 4.0. It was found by OpenAI Codex Security (Khai Tran and Sylvie Mayer) and fixed in the Node.js October 2026 security releases as GitHub Security Advisory GHSA-9v8h-qqgm-4m35. Its EPSS score is 88% (99.8th percentile), and CISA added it to the Known Exploited Vulnerabilities catalog on October 27, 2026, with exploitation observed in the wild from October 26. Affected releases are Node.js before 22.23.4, the 23.x and 24.x lines before 24.22.1, and the 25.x and 26.x lines before 26.12.1, all of which bundle Google Brotli 1.0.3 through 1.2.0; the same decoder also ships in .NET, Bun, Envoy, and Istio. Fixes are available in Node.js 22.23.4, 24.22.1, and 26.12.1, and administrators should upgrade immediately.

TECHNICAL OVERVIEW

Node.js bundles Google's Brotli library to decompress the br content coding. Any Node application that inflates an attacker-controlled Brotli stream reaches the vulnerable code: Express services using express.json() or body-parser with default inflation, code that reads a Brotli-encoded response through the built-in fetch(), and the many HTTP client and proxy libraries that embed the decoder. No authentication is involved, because decompression runs before any application logic. The flaw lives in the decoder's block-switch path in deps/brotli/c/dec/decode.c, exercised while the decoder walks the command stream of a metablock.

figure1.png
Figure 1: Node2Shell unauthenticated Brotli body drives an OOB write to a reverse shell

The root cause is an unchecked, overloaded return value. DecodeBlockTypeAndLength decodes a block-type switch and returns a BROTLI_BOOL. When a metablock declares only a single block type there is nothing to switch to, so the function returns BROTLI_FALSE to reject the request. That same BROTLI_FALSE is used elsewhere to mean "needs more input", so a single boolean conflates a fatal format error with a benign, recoverable condition.

figure2.png
Figure 2: DecodeBlockTypeAndLength returns the overloaded BROTLI_FALSE on an invalid block switch

That signal is then thrown away. The command-stream block switch is invoked through DecodeCommandBlockSwitch, which is declared static void and keeps no return value, and through the BROTLI_SAFE macro, whose non-safe branch simply calls the function and ignores what it returns. The BROTLI_FALSE from the invalid switch never stops decoding. The decoder proceeds as though the switch succeeded, with its block-length counter left at zero and its position and distance state carried forward on a path the format should have rejected.

figure3.png
Figure 3: DecodeCommandBlockSwitch is void so the BROTLI_FALSE error is discarded

Continuing in that corrupted state produces the out-of-bounds write. Later in ProcessCommandsInternal, a backward-reference copy computes its destination straight from the running position, copy_dst = &s->ringbuffer[pos], and performs a sixteen-byte vector store through memmove16. Because the earlier error was ignored, pos and the copy length are no longer constrained to the ring buffer, so the store lands outside the allocation. Under a debugger the faulting instruction is a single movups %xmm0,(%r8) with r8 equal to ringbuffer + pos: a sixteen-byte write whose destination the attacker steers through the preceding corruption.

figure4.png
Figure 4: memmove16 writes sixteen bytes to ringbuffer plus an attacker-influenced pos

Node.js 24.22.1 and the companion releases fix the flaw by replacing the overloaded boolean with an explicit status. DecodeBlockTypeAndLength now returns a BrotliDecoderErrorCode, and the invalid single-block-type case returns BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH instead of BROTLI_FALSE. The block-switch wrappers propagate that code, and a new BROTLI_SAFE_WITH_STATUS macro in ProcessCommandsInternal checks it and aborts decoding on any non-success result, so a malformed block switch now ends the stream cleanly rather than being silently ignored.

figure5.png
Figure 5: Node 24.22.1 returns an error code and checks it with BROTLI_SAFE_WITH_STATUS

TRIGGERING THE VULNERABILITY

The following conditions must be met for successful exploitation of CVE-2026-111497:

  • Network-Reachable Brotli Decompression: The target must be a Node.js service that decompresses an attacker-supplied Brotli stream, for example an Express endpoint using express.json() with default inflation, or code that reads a Brotli-encoded response through fetch(). The decode runs before authentication, so no credentials are required.
  • Vulnerable Version: The runtime must bundle Google Brotli 1.0.3 through 1.2.0, that is Node.js before 22.23.4, the 23.x and 24.x lines before 24.22.1, or the 25.x and 26.x lines before 26.12.1. Patched runtimes reject the stream with ERR__ERROR_FORMAT_BLOCK_SWITCH.
  • Single Block Type in the Metablock: The crafted metablock must declare a single block type, so that the command or distance block switch is invalid. That is the condition which makes DecodeBlockTypeAndLength return the overloaded BROTLI_FALSE.
  • Streaming (Chunked) Delivery: The body must arrive in stages so the decoder parks holding its partial input buffer between reads. The two-stage overflow depends on that pause; a single-shot decode of the whole input never separates the stages the corruption needs.
  • Oversized Block-Switch Trigger: The implicit distance block-length counter starts at 2^24 and must be fully drained, so the stream must carry at least 16,777,216 block-switch commands. That is a hard floor of roughly 2 MB even at one bit per command, and about 22 MB at the real encoding.

EXPLOITATION

Exploiting CVE-2026-111497 requires no authentication and no application-specific knowledge, only that the target decompresses a Brotli request or response. The attacker sends the crafted stream in two stages over one chunked HTTP request. The first stage is the oversized trigger that drains the block-length counter and drives the decoder into the invalid block switch, where the ignored error leaves it parked with a corrupted state and an eight-byte input buffer. After a brief pause the second stage appends a small chunk that writes past that eight-byte buffer, forging the decoder's position and distance fields. On the next copy the sixteen-byte store lands at an attacker-chosen address.

Video Demonstration

EXPLOIT PAYLOAD STRUCTURE

The request is an ordinary POST whose body is declared Content-Encoding: br and sent with Transfer-Encoding: chunked so the two stages can be delivered separately. The server treats it as a routine compressed upload and begins inflating it at once, which is why a successful attack looks like normal traffic until the write occurs.

figure6.png
Figure 6: Two-stage Brotli POST that forges decoder state for the out-of-bounds write
PAYLOAD KEY COMPONENTS
ComponentValuePurpose
TransportPOST with Content-Encoding: br and Transfer-Encoding: chunkedUnauthenticated path into the decoder; chunking separates the two stages
Stage 1 Trigger~22 MB stream, 2^24 block-switch commands, single block typeDrains the block-length counter and reaches the invalid block switch
Overloaded ReturnDecodeBlockTypeAndLength returns BROTLI_FALSEInvalid switch mis-signaled as recoverable, then discarded by the caller
Stage 2 OverflowSmall chunk appended past the 8-byte bufferForges pos and distance in the parked BrotliDecoderState
Write Primitivememmove16 to ringbuffer + posSixteen-byte out-of-bounds write-what-where
Code ExecutionOverwrite a saved return address, pivot to a ROP chain reaching execvpTurns the memory write into a reverse shell running as the service account

SONICWALL PROTECTIONS

To ensure SonicWall customers are prepared for any exploitation that may occur due to this vulnerability, the following signatures have been released:

Signature IDSignature Name
IPS: 22584Google Brotli Upload Informational
IPS: 22585Google Brotli Node id Command Execution (CVE-2026-111497) 1
IPS: 22588Google Brotli Node id Command Execution (CVE-2026-111497) 2
IPS: 22589Google Brotli Node id Command Execution (CVE-2026-111497) 3
IPS: 22591Google Brotli Node id Command Execution (CVE-2026-111497) 4
IPS: 22593Google Brotli Node id Command Execution (CVE-2026-111497) 5
SPY: 7371Malformed-br br.TL_1

REMEDIATION RECOMMENDATIONS

The risks posed by CVE-2026-111497 can be mitigated or eliminated with the following measures:

  • Upgrade Node.js: Move to Node.js 22.23.4, 24.22.1, 26.12.1, or later, where the Brotli decoder returns an explicit error code and the caller checks it. This is the direct fix and should be applied to every server-side runtime that inflates untrusted input.
  • Patch Embedded Decoders: The same Google Brotli decoder ships in .NET, Bun, Envoy, and Istio. Inventory those components and apply their vendor fixes, since the flaw is in the shared library rather than in Node.js alone.
  • Constrain Request Bodies: Cap the accepted request body size and disable automatic request-body inflation on endpoints that do not need it. A multi-megabyte Brotli-encoded request body has almost no legitimate use, because browsers negotiate Brotli for responses, not for uploads.
  • Monitor for Exploitation Artifacts: Alert on POST requests that carry Content-Encoding: br with an oversized or chunked body, and watch for unexpected outbound connections from application servers shortly after such a request, which would indicate a reverse shell.
  • Deploy IPS Signatures: Apply updated signature coverage at the perimeter and on any segment that can reach a Node.js listener, and keep signature sets current.
  • Segment the Network: Isolate application servers from sensitive internal resources and implement egress filtering to detect unauthorized outbound connections.

RELEVANT LINKS

ATTRIBUTION

The vulnerability was found by OpenAI Codex Security (Khai Tran and Sylvie Mayer) and disclosed through the Node.js project as GitHub Security Advisory GHSA-9v8h-qqgm-4m35. Fixes shipped in the Node.js October 2026 security releases (22.23.4, 24.22.1, and 26.12.1).

Third-party vulnerability database mirrors:

Share This Article

An Article By

Security News

The SonicWall Capture Labs Threat Research Team gathers, analyzes and vets cross-vector threat information from the SonicWall Capture Threat network, consisting of global devices and resources, including more than 1 million security sensors in nearly 200 countries and territories. The research team identifies, analyzes, and mitigates critical vulnerabilities and malware daily through in-depth research, which drives protection for all SonicWall customers. In addition to safeguarding networks globally, the research team supports the larger threat intelligence community by releasing weekly deep technical analyses of the most critical threats to small businesses, providing critical knowledge that defenders need to protect their networks.

Related Articles

  • PHP2Shell RCE is proven on PHP 8.5.11
    Read More
  • kotaemon check_connection Unauthenticated Deserialization RCE
    Read More