The two-stage local validation tool for CVE-2026-111497 (node2shell). It generates a full stream of the public PoC structure, feeding it to a pure C Brotli decoder, Node, Bun, and Python respectively; this stream is then analyzed for error semantics or memory corruption. **Scope**: Only validates trigger behavior, error semantics, and memory corruption; does not cover RCE, ROP, callbacks, or weaponization. Usage is restricted to isolated VMs, local files, or 127.0.0.1. ## Background
An out-of-bounds write vulnerability related to Brotli in the Node.js security patch cycle for October 2026. The public PoC splits the attack flow into two HTTP chunks:
- Stage one: 23,069,720 bytes, responsible for exhausting distance block counters and inducing “impossible block transitions”;
- Stage two: 27 bytes (poison) or 127 bytes (groom), continuing the same bitstream to influence decoder states. When only Stage one is fed, both the pure C ASAN and bare Node remain clean; writing occurs only after adding Stage two. This repository compiles the generation of both streams, C-level checks, and runtime validation into a reproducible script. ## Quick Start
This command produces `groom_full.bro` and `poison_full.bro` (each approximately 23MB), along with their corresponding SHA256 hashes. The stream originates from the `generate_stream`, `rce_stage_one`, and `rce_stage_two` functions within the public PoC `dimasma0305/CVE-2026-111497-POC`. The proof payload has been replaced with pattern data lacking any executable semantics. ### 2. Compiling the C harness
Brotli versions 1.2.0 (vulnerable) and 1.2.1 (fixed) source code are required:
bash build_harness.sh /path/to/brotli-1.2.0 /path/to/brotli-1.2.1
- `build-v120/harness_two_stage_plain_asan`
- `build-v121/harness_two_stage_plain_asan`
# Vulnerable version: Expected SEGV (uninitialized memory write via memmove16)
ASAN_OPTIONS=detect_leaks=0 ./build-v120/harness_two_stage_plain_asan poison_full.bro 27 16384 0x330
ASAN_OPTIONS=detect_leaks=0 ./build-v121/harness_two_stage_plain_asan poison_full.bro 27 16384 0x330
node verify_two_stage.js poison_full.bro 27
bun verify_two_stage_bun.js poison_full.bro 27
python3 verify_two_stage.py poison_full.bro 27
Single-stage comparison: use `poison_stream.bro` (only stage one) and pass `0`:
node verify_two_stage.js poison_stream.bro 0
python3 verify_two_stage.py poison_stream.bro 0
## Expected results (2026-10-30, Ubuntu 24.04 x86_64)
| Runtime | Version | Single-stage | Two-stage poison 27B |
| brotli C ASAN | 1.2.0 | Clean, NEEDS_MORE_INPUT | **SEGV in memmove16 (write)** |
| Node.js | 24.22.0 | Clean | **SIGSEGV, rc=139** |
| Bun | 1.4.3 | Z_BUF_ERROR, alive | Z_BUF_ERROR, alive (out 2048) |
| Python brotli | 1.1.0 | Silent, 1024B, is_finished=False | **SIGSEGV, rc=139** |
| Python brotli | 1.2.1 | Error | Error (decoder failed) |
Bun does not crash under two-stage streams, but the error semantics remain that of a vulnerable Z_BUF_ERROR; Python 1.1.0 directly suffers from a segmentation fault. Both indicate that the trigger key serves as an override for the second stage regarding the shared C library state; the actual behavior depends on the allocated memory and error handling mechanisms. ## Files
gen_rce_full.py # Two-stage flow generator
harness_two_stage.c # C-level two-stage feeding mechanism
build_harness.sh # Compilation of vulnerable/patched versions of the harness
verify_two_stage.js # Node.js validation script
verify_two_stage_bun.js # Bun validation script
verify_two_stage.py # Python validation script
├── stage2_trace.txt # Byte-by-byte APPEND/STATE logs
├── stage2_len_sweep.txt # Scanning of length N for the second stage
├── copy_trace_matrix.txt # Matrix of write addresses and number of iterations
- `BrotliDecoderState.buffer.u8` is 8 bytes; after invalid block switching triggers a NEEDS_MORE_INPUT condition, the second stage appends data byte by byte. This overwrites `buffer_length`, which in turn affects `pos`, `max_backward_distance`, and `max_distance`.
- A total of 27 bytes plus one byte at the end of Stage One results in exactly `max_distance`; only when N ≥ 23 can `max_backward_distance` be increased, thereby enabling distance checks via LZ77 copying mechanisms.
- The write address is computed as `ringbuffer + staged_pos − N×1024`; N is determined by the output flushing rate (0x330→8, ≥0x1000→20, public PoC Express→10, i.e., 0x2800).
- In version 1.2.1, the patched version returns -17 at the point of invalid block switching, preventing further appending cycles.
- The trigger flow structure originates from a public PoC:
- Node.js security advisory:
- Raw data was collected on an isolated VM on 2026-10-30; the ASLR address changes each time it runs. The address within “data/” is used solely for comparing the “pos” and “ringbuffer” structures during the same execution. This is neither a general detector nor a exploitation tool; do not use it on unauthorized targets. ## License
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
