Dissecting Node2Shell (CVE-2026-111497): Pre-Auth RCE in Node.js Brotli Decoder, Mitigation & Patching

A deep-dive technical analysis of Node2Shell (CVE-2026-111497), the critical CVSS 9.2 Out-of-Bounds Write flaw in Node.js bundled Google Brotli decoder (c/dec/decode.c) affecting Express 5, body-parser, and native fetch()—complete with C-level root cause, WAF rules, and patching steps.

Dissecting Node2Shell CVE-2026-111497 Pre-Auth RCE in Node.js Brotli Decoder
🚨
Critical Threat Advisory: Node2Shell (CVE-2026-111497) Tracked as CVE-2026-111497 (CVSS: 9.2 Critical) and dubbed Node2Shell, a C-level Out-of-Bounds Write (CWE-787) in the native Google Brotli decoder (c/dec/decode.c) bundled inside Node.js's zlib module enables pre-authentication Remote Code Execution (RCE) via crafted Content-Encoding: br HTTP requests or malicious fetch() responses. Patched in Node.js 26.12.1, 24.22.1, and 22.23.4.

October 21, 2026 will go down as one of the busiest patch days in modern web runtime history. On the very same day that the PHP Foundation shipped emergency patches for PHP2Shell (CVE-2026-109618 & CVE-2026-109619), the Node.js Project released security updates for CVE-2026-111497—a critical CVSS 9.2 memory corruption vulnerability in Node's bundled Brotli decompression engine dubbed Node2Shell by researchers at Imperva.

JavaScript developers are accustomed to thinking of Node.js as a memory-safe, garbage-collected environment where buffer overflows are a relic of C/C++ services. Node2Shell shatters that assumption. Because Node.js compiles and links C and C++ libraries directly into the runtime binary—including V8, libuv, OpenSSL, llhttp, and Google's reference C implementation of Brotli inside the native node:zlib module—a memory safety bug inside any of those C dependencies runs in the exact same OS process and address space as your Express, Fastify, or Next.js server.

Even worse, modern HTTP middleware (like Express 5's body-parser) and modern HTTP clients (like Node's native fetch()) automatically decompress Brotli streams before your JavaScript code ever sees the payload. In this technical deep dive, we dissect the C-level root cause of CVE-2026-111497 inside c/dec/decode.c, examine how both inbound requests and outbound fetch() calls expose your applications, and provide actionable patching and WAF mitigation playbooks.


1. The Two-Way Attack Surface: How Node.js Apps Trigger CVE-2026-111497

You do not need to explicitly import node:zlib or call zlib.brotliDecompress() in your own code to be vulnerable to Node2Shell. A standard Node.js web service is exposed across two automatic HTTP vectors:

VECTOR 1: Inbound HTTP Request Body (Server-Side Pre-Auth RCE)
  Attacker ──[ POST /api/login | Content-Encoding: br ]──► Express 5 / body-parser (2.0.0–2.3.0)
                                                                 │
                                                    zlib.createBrotliDecompress()
                                                                 │
                                                    deps/brotli/c/dec/decode.c
                                                                 │
                                                    💥 Out-of-Bounds Write (CWE-787) -> RCE

VECTOR 2: Outbound HTTP Client Fetch (Client-Side / SSRF / Webhook RCE)
  Node.js App ──[ await fetch(untrustedUrl) ]──► Attacker Server returns Content-Encoding: br
                                                                 │
                                                    Undici automatic Brotli decompression
                                                                 │
                                                    💥 Out-of-Bounds Write (CWE-787) -> RCE

Vector 1: Inbound Request Decompression (Express 5 & body-parser 2.x)

When you mount app.use(express.json()) or app.use(express.urlencoded()) in an Express application, the underlying body-parser middleware defaults to inflate: true. Beginning with body-parser 2.0.0 through 2.3.0 (standard in Express 5.0.0–5.3.0), the middleware automatically inspects the incoming Content-Encoding header and pipes any request with Content-Encoding: br directly into Node's native zlib.createBrotliDecompress() stream.

Because body parsing typically runs as global middleware—or runs before controller-level authentication and schema validation (such as Zod or Joi)—an unauthenticated attacker can send a single POST request with Content-Encoding: br to trigger the vulnerable C decoder before a single line of route logic executes.

Vector 2: Outbound fetch() & Automatic Response Decompression

What if your API doesn't accept compressed request bodies? If your Node.js service makes outbound HTTP calls to user-supplied or third-party URLs—such as webhook verifiers, OpenGraph link previewers, OIDC discovery endpoints, RSS aggregators, or AI web-scraping pipelines—Node's built-in fetch() (powered by undici) automatically advertises Accept-Encoding: gzip, deflate, br and transparently pipes Content-Encoding: br responses through the exact same native Brotli decoder.


2. C-Level Root Cause: Dissecting c/dec/decode.c & Commit 0ed4268

CVE-2026-111497 resides in Google's upstream Brotli reference library (versions 1.0.3 through 1.2.0), specifically inside the core decompression state machine in c/dec/decode.c.

How Brotli Block-Type Switching Works

Under RFC 7932 (Brotli Compressed Data Format), a compressed stream is composed of meta-blocks. Within each meta-block, symbols are divided into three block categories:

  1. Literal blocks (Category 0)
  2. Insert-and-copy command blocks (Category 1)
  3. Distance blocks (Category 2)

Each category can define between 1 and 256 distinct block types (num_block_types[i]), allowing the encoder to switch Huffman trees mid-stream when the statistical properties of the data change. To track these switches without constant heap allocations, the C struct BrotliDecoderStateStruct (c/dec/state.h) maintains internal state arrays, ringbuffers (block_type_rb), and Huffman table pointers on the heap:

/* Simplified layout inside BrotliDecoderStateStruct (c/dec/state.h) */
struct BrotliDecoderStateStruct {
    BrotliRunningState state;
    int loop_counter;
    const uint8_t* pos;
    /* ... */
    uint8_t* ringbuffer;
    uint8_t* ringbuffer_end;
    HuffmanCode* htree_command;
    uint32_t block_type_rb[6];     /* 2-element history per block category (0, 1, 2) */
    uint32_t num_block_types[3];   /* Number of block types declared for each category */
    HuffmanCode* block_type_trees;
    HuffmanCode* block_len_trees;
    brotli_alloc_func alloc_func;  /* ⚠️ Custom allocator function pointer on the struct! */
    brotli_free_func  free_func;   /* ⚠️ Custom deallocator function pointer on the struct! */
    void* memory_manager_opaque;
    /* ... */
};
🔬
The Out-of-Bounds Write Primitive (CWE-787) When a crafted Brotli bitstream encodes an invalid block-type switch sequence in c/dec/decode.c, the unpatched state machine fails to verify the block-switch validity invariant early enough. The decoder computes an out-of-bounds offset and writes past the allocated Huffman/state buffer directly into adjacent heap memory inside or bordering BrotliDecoderStateStruct.

From Out-of-Bounds Write to Remote Code Execution in Node.js

Why is an out-of-bounds write inside BrotliDecoderStateStruct so lethal inside Node.js?

  • Adjacent Function Pointers & C++ Vtables: Look closely at the fields inside BrotliDecoderStateStruct and its surrounding C++ wrapper (node::zlib::BrotliDecoder in Node's src/node_zlib.cc). The decoder state lives on the standard C++/heap allocator alongside C++ objects containing virtual method tables (vtable pointers) and custom memory management callbacks (alloc_func / free_func).
  • Controlled Stream Cleanup: When a stream finishes or errors out, BrotliDecoderDestroyInstance() iterates over allocated sub-structures and invokes s->free_func(s->memory_manager_opaque, address). Overwriting free_func and memory_manager_opaque—or corrupting ringbuffer pointers so subsequent uncompressed literal bytes write to an arbitrary address (Write-What-Where)—gives the attacker direct instruction pointer (RIP / PC) control.
  • Real-World RCE Validation: Researchers on the Imperva Red Team built a standalone proof-of-concept and confirmed full Remote Code Execution against official containerized Node.js builds (node:24.22.0 on Linux ARM64) running Express 5.x and body-parser 2.x.

How Upstream Commit 0ed4268 Fixes the Flaw

The upstream fix in Google Brotli commit 0ed4268 ("Detect and report invalid stream earlier") adds strict precondition validation inside c/dec/decode.c and introduces a dedicated error code in c/include/brotli/decode.h:

/* Added in c/include/brotli/decode.h & enforced in c/dec/decode.c (Commit 0ed4268) */
BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH = -17,

/* In c/dec/decode.c: If an invalid block switch is encountered in the bitstream,
   immediately abort with BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH)
   before indexing or writing into decoder state buffers. */

3. Affected & Patched Version Matrix

Both the Node.js runtime and standalone installations of Google's libbrotli C library must be updated. Note that End-of-Life (EOL) Node.js branches (**Node 20.x, 18.x, 16.x**, and odd-numbered releases **23.x / 25.x**) will **not** receive official patches from the Node.js project.

Runtime / Library Release Line Vulnerable Versions Patched Version
Node.js 26.x Current Release < 26.12.1 v26.12.1+
Node.js 24.x Active LTS < 24.22.1 v24.22.1+
Node.js 22.x Maintenance LTS < 22.23.4 v22.23.4+
Node.js 20.x / 18.x / 23.x / 25.x End-of-Life (EOL) All releases bundling Brotli 1.0.3–1.2.0 No upstream patch (Migrate to 22 / 24 / 26)
Google Brotli (C Library) Upstream libbrotlidec 1.0.3 – 1.2.0 1.0.10 / 1.1.1 / 1.2.1+ (Commit 0ed4268)

How to Check Your Bundled Brotli Version in Node.js

You can inspect the exact version of Brotli compiled into your running Node.js binary with a single command:

node -p " ({ node: process.version, brotli: process.versions.brotli }) "
# If node is < 22.23.4 / 24.22.1 / 26.12.1 and brotli is 1.0.3–1.2.0, you are VULNERABLE.

Note for Linux Distro Packages: Some Linux distributions (such as Alpine, Debian, and Fedora) compile their system nodejs package with --shared-brotli, dynamically linking against the OS's libbrotlidec.so instead of Node's bundled copy. In those environments, run apt upgrade libbrotli1 nodejs or apk upgrade brotli-libs nodejs and restart all Node processes.


4. Defense-in-Depth Mitigations & Virtual Patching

If you cannot immediately redeploy every container or must protect legacy Node.js 18/20 services while migrating, apply these three compensating controls immediately:

A. Express / Fastify Application-Level Middleware Guard

Virtually no standard web browser compresses outbound POST or PUT form/JSON requests with Brotli (Content-Encoding: br is overwhelmingly used for server-to-browser *responses*, not browser-to-server *requests*). You can block inbound Content-Encoding: br requests **before** express.json() or body-parser touches the stream—or disable automatic request inflation entirely:

// Option 1: Reject inbound Content-Encoding: br BEFORE mounting body-parser / express.json()
app.use((req, res, next) => {
  const encoding = (req.headers['content-encoding'] || '').toLowerCase();
  if (encoding.includes('br')) {
    return res.status(415).json({ error: 'Brotli request body encoding is not supported' });
  }
  next();
});

// Option 2: Or disable automatic body decompression in express.json() / express.urlencoded()
app.use(express.json({ inflate: false }));
app.use(express.urlencoded({ extended: true, inflate: false }));

B. Hardening Outbound fetch() Calls Against Malicious Upstreams

If your Node.js service fetches untrusted external URLs (webhooks, link previews, scrapers), override the Accept-Encoding header on those requests so upstream servers only send gzip or uncompressed payloads:

const response = await fetch(untrustedWebhookUrl, {
  headers: {
    'Accept-Encoding': 'gzip, deflate' // Explicitly omit 'br' on untrusted outbound calls
  }
});

C. Edge Reverse Proxy (Nginx) & WAF (ModSecurity / Coraza / Centinela) Rules

Block inbound requests carrying Content-Encoding: br at your ingress proxy or WAF so malformed Brotli streams never reach your Node.js containers:

1. Nginx Ingress / Reverse Proxy:

# Reject client requests compressed with Brotli (does not affect server-to-client br responses!)
if ($http_content_encoding ~* "\bbr\b") {
    return 415;
}

2. ModSecurity / Coraza / OWASP CRS SecRule:

# Block inbound HTTP requests specifying Content-Encoding: br (CVE-2026-111497 / Node2Shell)
SecRule REQUEST_HEADERS:Content-Encoding "(?i)\bbr\b" \
    "id:1011497,phase:1,deny,status:415,log,t:none,msg:'Node2Shell (CVE-2026-111497) Inbound Brotli Request Blocked'"

5. Post-Patch Threat Hunting & Crash Auditing

Because CVE-2026-111497 is a C-level heap buffer overflow inside the Node.js process, unsuccessful or probing exploit attempts almost always corrupt adjacent heap metadata and crash the Node.js worker with SIGSEGV (exit code 139) or SIGABRT (free(): invalid pointer / corrupted size vs. prev_size):

  1. Audit Access Logs for POST/PUT Requests with 415, 502, or 500 Statuses:When a Node.js backend segfaults mid-request while parsing a Content-Encoding: br body, Nginx or your load balancer logs an upstream premature disconnect (502 Bad Gateway).

Inspect Child Processes Spawned by node:

ps auxf | grep -A 5 -E "\bnode\b"

Check Kubernetes / Docker / Systemd Logs for Node Crashes:

# Check systemd journal for segmentation faults or heap corruption aborts in node processes
journalctl -u my-node-service --since "7 days ago" | grep -E "SIGSEGV|SIGABRT|corrupted|invalid pointer|core dumped"

# Check Kubernetes pods for exit code 139 (SIGSEGV) or 134 (SIGABRT) restarts
kubectl get pods -A -o json | jq -r '.items[] | select(.status.containerStatuses[]?.lastState.terminated.exitCode == 139 or .status.containerStatuses[]?.lastState.terminated.exitCode == 134) | .metadata.name'

Summary

Node2Shell (CVE-2026-111497) is a textbook example of how native C dependencies hidden beneath high-level JavaScript APIs can introduce critical pre-authentication memory corruption risks. Because both Express 5 (body-parser 2.x) and Node's built-in fetch() automatically pipe Content-Encoding: br streams into deps/brotli/c/dec/decode.c, patching cannot wait.

Upgrade your Node.js runtimes to v26.12.1, v24.22.1, or v22.23.4 today, reject unsolicited inbound Content-Encoding: br headers at your reverse proxy or WAF, and migrate any remaining Node 18 or Node 20 workloads onto an actively supported LTS release.

Read more