The Node.js Brotli flaw known as Node2Shell (CVE-2026-111497) has moved from proof-of-concept to active attack. European threat-intelligence firm ELLIO says its sensors recorded the first real exploitation attempt on 26 October 2026 at 15:08 UTC — a single Brotli-compressed HTTP request body of just over 23 MB, carrying a working remote-code-execution chain aimed at one exact target: Node.js v24.22.0 on x86-64. Until that request landed, every public tracker, including the CISA entry in the CVE record, still listed exploitation as “none”.
ELLIO researcher Vlad Iliushin, who flagged the activity, said the attempts hit the firm’s deception network and were validated against unpatched builds in the lab, where the same payloads returned a shell. The company is now blunt about the status: treat Node2Shell as exploited and patch now.
The vulnerability is an out-of-bounds write (CWE-787) in Google’s Brotli decompression library, the C code that Node.js bundles under deps/brotli rather than pulling from npm. Google, which acts as the CVE Numbering Authority for Brotli, rates it 9.2 Critical on CVSS 4.0; the Node.js project’s own advisory rates it High and states plainly that the flaw permits remote code execution. It affects Brotli 1.0.3 through 1.2.0, which means the exposure reaches far past Node.js — into Bun, Python packages, Microsoft’s .NET and PowerShell, Envoy, Ruby and distro-level system libraries.
What the bug actually does
A Brotli stream is built from blocks, and the decoder reads a “block switch” whenever one run of symbols ends, and the next begins. The flaw appears when a stream declares only a single block type for a given kind and then tries to switch anyway — an input that is not a valid Brotli and should be rejected outright. Instead, a helper function returned a plain false both for that invalid switch and for the ordinary case where the decoder has simply run out of bytes mid-read. Two different faults, one answer.
Because the caller cannot tell the two apart, it treats the invalid switch as a request for more input and keeps feeding bytes into an eight-byte internal buffer that never drains. Past the eighth byte, the writes land outside it.
As the Node.js advisory describes it, an invalid block switch “was reported as a temporary need for more input instead of a permanent format error,” which is exactly the condition that lets an attacker who controls the stream write their own bytes to a chosen location in memory. The upstream fix, commit 0ed4268a, gives the helper its own error type so the decoder stops on an invalid stream instead of asking for more data.
One trap for defenders checking their exposure: Node.js cherry-picked the fix without bumping the bundled version string, so process.versions.brotli still reports 1.2.0 on both a patched and an unpatched build. The release version is the only reliable signal.
From crash to shell — and why it took a lab to prove it
The first public demonstration that the bug was more than a crash came from Imperva’s Red Team, which gave the flaw the Node2Shell name. On 22 October, a day after the patch shipped, Imperva researcher Ron Masas showed code execution against the official, unmodified Node.js 24.22.0 image on Linux arm64, reaching the decoder both through the runtime’s built-in fetch() and through Express 5 request bodies, using no debugger or instrumented binary. Imperva tested Express 5.0.0 through 5.3.0 with body-parser 2.0.0 through 2.3.0, and noted that execution took multiple attempts.
ELLIO then answered the three questions Imperva’s work left open — whether it works on x86-64, whether it holds across Node builds, and whether the framework in front of the decoder matters. Its team reproduced RCE on x86-64 against four targets running the same build: a plain node:http server calling node:zlib, the official node:24.22.0-slim image running as root, Express 5.3.0 with express.json(), and Koa 3 with @koa/bodyparser. Each returned a shell running as the service’s own account, answering id and uname -r back over the same connection that delivered the payload. The patched 24.22.1 release rejected every input with a 400 and a block-switch format error.
Reliability is the catch, and it is the detail that matters most for defenders. ELLIO found that each Node.js build needs its own memory offset; the value that gave a shell on 24.22.0 produced nothing in 10,000 attempts against 22.23.3 and 26.12.0. Counterintuitively, leaving address space layout randomization (ASLR) enabled made exploitation easier, not harder — a shell arrived around the fifth attempt with ASLR on, while 60 tries with ASLR disabled produced no hit, because a single frozen layout that an attacker measures once removes the restart-loop lottery entirely. The process supervisor who keeps a service running is the one who hands the attacker fresh layout after fresh layout.
That same restart loop turns every failed attempt into a problem of its own. In ELLIO’s lab, most of about four hundred attempts against Express crashed the worker instead of returning a shell — and on a stock Ubuntu host where systemd-coredump is active, each crash can write a core dump containing whatever was live in memory at that instant, including another visitor’s session cookies, bearer tokens or key material. A service with no restart supervisor at all does not need an exploit chain: ELLIO found the second malformed request simply crashed it and kept it down.
The attack in the wild, and who is behind it
The reconnaissance phase was visible well before the exploit. A public exposure checker calling itself node2shell-check reached ELLIO’s sensors from an address in Turkey, sending batches of Brotli bodies designed to map which hosts decompress Brotli at all rather than to exploit them — the methodical hour-before-a-wave footprint of someone building a target list.
The 26 October request was different. ELLIO describes it as a 23,069,983-byte body hashing to a known SHA-256 value, with the attacker’s memory-overwrite instructions and the decoder-targeting stage glued into a single HTTP request where public tooling sends them as two pieces.
The delivery layer is a straight copy of the public proof of concept, but the ROP chain — the part that actually seizes control of the process — is original, built on three gadgets ELLIO says it had not seen published, planting its own /bin/sh string and calling Node’s own execvp on it. Every address in the chain resolves in exactly one binary: the official Node.js v24.22.0 x86-64 release.
ELLIO traced the source to 94.154.43.7, an address it rates malicious with high confidence, first seen on 15 December 2025 and active on 47 of the preceding 90 days. Its record already carries tags for PHP2Shell exploitation, Palo Alto GlobalProtect and FortiOS VPN probing, and Kubernetes scanning, along with six prior CVEs — the profile, ELLIO says, of an operator focused on mass exploitation rather than a one-off researcher.
The fact that the chain is version-pinned fits that model: measure the offset once for a popular build, then reuse it against every host that advertises that version in a banner, a Server header, a lockfile or a container tag.
Who needs to patch, and what to do now
A bare Node.js HTTP server does not inflate an incoming Brotli body just by receiving it — something has to hand the bytes to the decoder. That happens on inbound requests through Express 5, body-parser 2 or Koa’s body parser when a client sends Content-Encoding: br; on outbound responses that undici-based clients, including built-in fetch(), axios, got and server-side fetches in frameworks such as Next.js, decode automatically, and through any direct node:zlib Brotli call on client-supplied data.
The fixes are Node.js 22.23.4, 24.22.1 or 26.12.1. The 20.x, 23.x and 25.x lines are end of life and will not be patched, so applications on those lines must move to a supported release. Node.js offers no official workaround; where patching cannot happen immediately, ELLIO and others advise dropping Content-Encoding: br on inbound requests at a proxy or middleware, which closes the request-body path but not the responses an application fetches.
Crucially, upgrading Node.js only fixes the copy of the decoder Node bundles. The same vulnerable library is compiled into Bun before 1.4.4, the PyPI Brotli and brotlicffi packages, Envoy, the Ruby brotli gem, and distribution system libraries — each of which needs its own update. Patched versions have already propagated into downstream projects from code-server to Vendure and Trigger.dev, most of them simply rebasing onto the fixed Node runtime.
There is a genuine disconnect in the record that readers checking their own risk ratings should know about. Multiple vulnerability trackers, drawing on CISA’s Stakeholder-Specific Vulnerability Categorization entry dated 23 October, still list exploitation as “none,” automatable as “no,” and the flaw as absent from the CISA Known Exploited Vulnerabilities catalogue. Those assessments predate ELLIO’s 26 October observation, and they have not yet caught up.
There is also a credit that the two camps record differently. ELLIO and several trackers attribute the original report to OpenAI Codex Security researchers Khai Tran and Sylvie Mayer, while the Node.js advisory credits Matteo Collina with reporting it and Antoine du Hamel with the fix; the Broccoli2Shell nickname is credited to Juliano Rizzo.
Neither Node.js nor the attacker’s host has issued any public statement on the first in-the-wild attempt beyond the telemetry ELLIO has published, and whether 94.154.43.7’s operator moves from a single pinned build to broader, multi-version waves is the open question the next few days will answer.
Community Discussion
Join the conversation. Ask questions, share solutions, and help others.
Be the first to start the discussion!
