DEV Community

syrex1013
syrex1013

Posted on

Node2Shell: Memory Corruption RCE in Brotli Affecting Node.js

Node2Shell: Memory Corruption RCE in Brotli Affecting Node.js

I recently published a proof of concept for CVE-2026-111497, which I’ve been calling Node2Shell.

The issue is a memory-corruption vulnerability in brotli 1.2.0 that affects Node.js releases bundling the vulnerable decoder.

The interesting part is that this is not an application-level eval(), execSync(), child-process misuse, or webhook trick.

The vulnerability exists lower in the stack, inside the Brotli decoder itself.

Under the right conditions, corrupted decoder state can eventually redirect execution through a callback path and reach system().

Affected Node.js Versions

According to the affected ranges identified during testing:

  • Node.js 22.x before 22.23.4
  • Node.js 24.x before 24.22.1
  • Node.js 26.x before 26.12.1

The underlying issue originates in the bundled Brotli implementation.

Root Cause

One part of the problem is located around DecodeBlockTypeAndLength().

Under specific decoder state conditions, the function returns:

BROTLI_FALSE
Enter fullscreen mode Exit fullscreen mode

The important detail is that this value is 0.

Elsewhere in the decoder, 0 overlaps with the numeric value used for:

BROTLI_DECODER_SUCCESS
Enter fullscreen mode Exit fullscreen mode

The BROTLI_SAFE handling path interprets that result incorrectly and treats the state as though the decoder simply needs more input.

The decoder then saves its state around:

COMMAND_BEGIN
Enter fullscreen mode Exit fullscreen mode

and re-enters the same path.

This can result in a persistent CPU loop.

At this point, the vulnerability already provides a denial-of-service condition.

But the more interesting behavior appears in another decoder path.

The Out-of-Bounds Write

When the decoder reaches a NEEDS_MORE_INPUT condition while:

buffer_length == 0
Enter fullscreen mode Exit fullscreen mode

a copy-tail operation runs.

Conceptually, the relevant code behaves like this:

s->buffer.u8[s->buffer_length] = *next_in++;
s->buffer_length++;
Enter fullscreen mode Exit fullscreen mode

There is no effective release-build bounds check protecting the write.

A BROTLI_DCHECK exists around some of this logic, but debug assertions do not provide protection in production builds.

That means decoder-controlled state can cause writes beyond the intended small internal buffer.

Why Input Size Matters

One of the key constraints during exploitation is:

BROTLI_FAST_INPUT_SLACK = 28
Enter fullscreen mode Exit fullscreen mode

Input size changes the decoder path significantly.

With sufficiently small chunks, the decoder can be forced into a state where command processing exits early with a "need more input" result.

That feeds execution back into the vulnerable copy-tail logic.

With larger chunks, the decoder is more likely to fall into the CPU-spin behavior instead.

In practice, this means the malformed input must be delivered in carefully sized fragments rather than as one large buffer.

This distinction turned out to be critical while reproducing the issue.

Corrupting Decoder State

Once the out-of-bounds write is reachable, the next question is what sits next to the internal buffer.

The decoder state contains additional fields immediately after it.

By extending the corruption far enough, an attacker can begin modifying adjacent decoder state, including pointer-sized values and callback-related data.

One particularly interesting target is the metadata callback state.

Conceptually, the exploitation chain becomes:

crafted Brotli stream
        ↓
decoder state confusion
        ↓
NEEDS_MORE_INPUT path
        ↓
out-of-bounds state corruption
        ↓
callback state overwritten
        ↓
controlled callback invocation
        ↓
code execution
Enter fullscreen mode Exit fullscreen mode

During testing, the callback path could be redirected so that execution reached system() with attacker-controlled callback data.

This produces native process execution without relying on JavaScript execution primitives.

Why This Is Interesting

Most Node.js RCE demonstrations eventually reduce to something like:

require("child_process").exec(...)
Enter fullscreen mode Exit fullscreen mode

or:

eval(...)
Enter fullscreen mode Exit fullscreen mode

Node2Shell is different.

The execution path exists below the JavaScript layer.

The application gives compressed data to the runtime.

The runtime passes it to Brotli.

The decoder corrupts its own native state.

Native control flow is then redirected.

That makes the bug much closer to a traditional native memory-corruption vulnerability than a typical Node.js application bug.

Proof of Concept

I published the proof of concept here:

github.com/syrex1013/CVE-2026-111497-node2shell
Enter fullscreen mode Exit fullscreen mode

The PoC demonstrates the decoder behavior and the vulnerability chain used during the research.

A successful test produced native command execution from the affected process.

Simplified Vulnerability Chain

The overall chain can be summarized as:

  1. Trigger the problematic Brotli decoder state.
  2. Force the decoder into the NEEDS_MORE_INPUT path.
  3. Deliver input in small fragments.
  4. Reach the vulnerable copy-tail logic.
  5. Corrupt adjacent Brotli decoder state.
  6. Modify callback-related state.
  7. Trigger the metadata callback.
  8. Redirect native execution.

The two observable outcomes are therefore closely related:

malformed stream
     │
     ├── larger input/state path → infinite decoder loop → DoS
     │
     └── chunked input path → memory corruption → native code execution
Enter fullscreen mode Exit fullscreen mode

Defensive Takeaways

If you operate Node.js services that consume Brotli-compressed input, updating the runtime should be treated as the primary mitigation.

Additional defensive measures include:

  • upgrading Node.js to a patched release;
  • limiting exposure of Brotli decompression endpoints;
  • enforcing request and decompression limits;
  • isolating Node.js workloads with containers or sandboxing;
  • running services without unnecessary privileges;
  • monitoring for abnormal CPU saturation during decompression;
  • monitoring crashes or memory-corruption indicators around Brotli processing.

Memory-safety issues in compression libraries are especially important because decompression often happens before application-level validation.

Final Thoughts

Node2Shell is a useful example of why a memory-unsafe dependency deep inside a modern runtime can become security-critical even when the surrounding application code is completely ordinary.

The vulnerability does not require an obvious JavaScript execution sink.

Instead, the interesting behavior comes from a combination of:

  • incorrect decoder-state handling;
  • ambiguous return-value semantics;
  • release-build assumptions;
  • an out-of-bounds write;
  • adjacent callback state;
  • and precise control over streaming input.

That combination turns what initially looks like a decoder hang into a much more serious native memory-corruption problem.

PoC:

github.com/syrex1013/CVE-2026-111497-node2shell

For security research and authorized testing only.

nodejs #security #cybersecurity #cve

Top comments (0)