SecPod

Learn Search

Search across all Learn content

← Back to Security Research
Node2Shell: Exploiting CVE-2026-111497 Through Malformed Brotli Streams in Node.js

Node2Shell: Exploiting CVE-2026-111497 Through Malformed Brotli Streams in Node.js

Node2Shell (CVE-2026-111497) is a Brotli decoder out-of-bounds write in Node.js with a public RCE exploit. Analysis, IOCs, detection queries and fixes.

Oct 27, 2026By Smayan C Nandi
vulnerability research

Node2Shell: Exploiting CVE-2026-111497 Through Malformed Brotli Streams in Node.js

October 28, 2026 CVSS v4.0 - 9.2 Node.js Rating: High Exploitation Attempts Observed in the Wild Public RCE Exploit Available CISA KEV Listed (Oct 27, 2026) Patch Available Vulnerability Research

Summary: CVE-2026-111497, nicknamed Node2Shell (later also HTTP2Shell and Broccoli2Shell), is a memory-corruption flaw in Google's Brotli decompression library, which Node.js compiles directly into its binary. A crafted Brotli stream drives the decoder into a block-type switch that the stream's own header makes impossible. The vulnerable decoder misreads that format error as "I need more input", keeps appending attacker bytes into an 8-byte internal buffer, and writes past it into adjacent decoder state.

Independent researchers have turned this into code execution on unmodified official Node.js 24.22.0 builds on both arm64 and x86-64, reaching the decoder through the built-in fetch() (a malicious server's response), through Express 5 / body-parser 2 and Koa 3 request bodies sent with Content-Encoding: br, and even through a plain node:http server that calls node:zlib. A public RCE exploit released by researcher dimasma0305 is circulating, exposure scanning has started, the first in-the-wild exploitation attempt was recorded on October 26, 2026, and CISA added the CVE to its Known Exploited Vulnerabilities (KEV) catalog on October 27, 2026 with a federal remediation due date of November 10, 2026. No authentication, no user interaction.

Fix: Node.js 22.23.4, 24.22.1, 26.12.1. EoL lines (20.x, 23.x, 25.x) receive nothing. The same decoder ships in Python Brotli, brotlicffi, Bun, Envoy, .NET and curl builds linked against libbrotli, all of which need their own updates.

Overview

Vulnerability Name
Node2Shell / HTTP2Shell / Broccoli2Shell: Brotli decoder out-of-bounds write permits remote code execution
CVE ID
CVE-2026-111497
Weakness
CWE-252 (Unchecked Return Value) → CWE-787 (Out-of-Bounds Write)
Severity
CVSS v4.0 9.2 Critical (Google, CNA)  |  Node.js advisory: High  |  Microsoft (.NET): CVSS v3.1 8.1
CVSS v4.0 Vector
AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Vulnerable Component
Google Brotli c/dec/decode.c, function DecodeBlockTypeAndLength(), reached via BrotliDecoderDecompressStream()
Affected Products
Node.js 22.x < 22.23.4, 24.x < 24.22.1, 26.x < 26.12.1, all EoL lines; Google Brotli 1.0.3–1.0.9, 1.1.0, 1.2.0; Python Brotli ≤ 1.2.0 (including older releases such as 0.5.2, 0.6.0 and 1.0.1); Python brotlicffi ≤ 1.2.0.2 (including older releases such as 0.8.0); Bun 1.0.21 to before 1.4.4; Envoy with Brotli decompressor; .NET 8/9/10 runtimes and .NET 11 previews
Discovered / Credited
Reported by Khai Tran and Sylvie Mayer of OpenAI Codex Security. Fixed in Node.js by Matteo Collina (mcollina) and Antoine du Hamel (aduh95), per the official Node.js advisory.
Published
October 21, 2026 (Node.js security release and Brotli 1.0.10 / 1.1.1 / 1.2.1)
Short Description
A crafted Brotli stream causes the streaming decoder to ignore an invalid block-switch condition, buffer attacker data past an 8-byte internal buffer and corrupt decoder state, leading to crash, memory disclosure or arbitrary code execution inside the process that decompresses it.
9.2
CVSS v4.0 base score
236 days
Fix in Brotli source → first release shipping it
5 days
Disclosure → first in-the-wild exploitation attempt
CISA KEV
Added Oct 27, 2026; federal due date Nov 10, 2026
6+
Ecosystems bundling the vulnerable decoder
0
Supported workarounds per the Node.js advisory

Why This Bug Matters: Decompression Is an Attack Surface

Brotli (Content-Encoding: br) is the default high-ratio compression on the modern web. Every CDN, browser and most HTTP clients speak it, which means it is also something servers and clients decode automatically, often without application code ever asking for it. Node.js exposes the bundled decoder through node:zlib (brotliDecompress, createBrotliDecompress), and higher-level code reaches it implicitly: fetch() (Undici) transparently decompresses responses, and Express 5 / body-parser 2 decompress br request bodies before the JSON or raw parser ever sees them.

That makes CVE-2026-111497 unusual among runtime bugs. The attacker does not need a vulnerable application route or a logic bug in your code. They need one of two things: a way to send a compressed body to a parser that inflates it, or a way to make your service fetch something from a server they control. Webhooks, URL previews, OAuth discovery, SSRF-ish "import from URL" features, link unfurlers, scraper services and server-side rendering all qualify.

Two independent routes into the same native decoder Attacker client POST + Content-Encoding: br Express 5.0.0–5.3.0 body-parser 2.x raw() / json() Attacker server Response: Content-Encoding: br fetch() / Undici webhooks, previews, imports Direct node:zlib use uploads, archives, caches node_zlib.cc CompressionStream::Write() Bundled Brotli decode.c BrotliDecoderDecompressStream() Out-of-bounds write crash (DoS) memory disclosure code execution A bare node:http server does NOT inflate request bodies on its own; exposure depends on what decodes Brotli. Outbound fetch() exposure exists regardless of what Accept-Encoding you send if the remote server is hostile.
Figure 1: Node2Shell attack surface. Both inbound request bodies and outbound responses funnel into the same vulnerable native decoder.

Root Cause Analysis

Brotli in 90 seconds

A Brotli stream is a sequence of meta-blocks. Each meta-block header declares, for three categories (literals, insert-and-copy commands, distances), how many block types exist (NBLTYPES, 1 to 256) and, when more than one exists, a prefix code for block-type switch commands and block lengths. During decoding, when the current block's counter runs out, the decoder calls DecodeBlockTypeAndLength() to read the next block type and its length from the bitstream, then swaps in the matching Huffman tree group and context map.

If a category declares exactly one block type, a block switch is structurally impossible: there is nothing to switch to, and no switch codes were ever decoded. A conforming decoder must treat any attempt to perform one as a format error.

The flaw: a format error that looks like "need more input"

DecodeBlockTypeAndLength() returns a boolean-style result. In vulnerable versions, the path that encounters the impossible switch returned the same failure value as the path that simply ran out of buffered bits. The caller, the streaming state machine in BrotliDecoderDecompressStream(), interprets "false" as BROTLI_DECODER_NEEDS_MORE_INPUT: it saves its position, stashes the unconsumed tail of input into a small internal ring and returns to the host asking for more bytes. That is the CWE-252 half of the bug.

The CWE-787 half follows directly. The decoder keeps a tiny internal buffer (8 bytes, sized for the largest bit-reader refill it should ever need) to carry partial input across calls. In the "need more input" path it appends new caller data into that buffer, expecting a successful parse to drain it. Because the parse can never succeed on this stream, every subsequent chunk from the attacker is appended past the end of the 8-byte buffer, overwriting whatever decoder state follows it in the BrotliDecoderState structure: pointers, counters, ring-buffer sizes and table references the decoder will later dereference and write through.

Vulnerable (Brotli ≤ 1.2.0) Patched (commit 0ed4268, Brotli 1.2.1) Meta-block header: NBLTYPES = 1 no switch codes decoded Block length exhausted → DecodeBlockTypeAndLength() Switch impossible returns plain FALSE Caller assumes NEEDS_MORE_INPUT state preserved, asks for next chunk Next chunk appended to 8-byte buffer no bound check, parse never succeeds OOB write into BrotliDecoderState corrupted pointers → write primitive loop Meta-block header: NBLTYPES = 1 same malicious input Block length exhausted → DecodeBlockTypeAndLength() Switch impossible distinct error result BROTLI_DECODER_ERROR_ FORMAT_BLOCK_SWITCH Error propagated to caller decoder enters terminal error state Node.js rejects stream no write, process keeps running
Figure 2: Decoder state machine on the impossible block switch, before and after the fix.

Memory view of the corruption

BrotliDecoderState (simplified, illustrative field order) bit reader val / bit_pos buffer[8] partial-input carry counters / block lengths ring buffer ptr / size & mask Huffman table / context map ptrs allocator callbacks / opaque attacker bytes appended chunk after chunk, overflowing into every field to the right byte 8: bound crossed Overwriting the ring-buffer pointer and mask turns ordinary decoder output writes into an attacker-steered write primitive. Overwriting table pointers or allocator callbacks gives control over later reads, frees or indirect calls. Field order is schematic for illustration; consult decode.c / state.h of the exact Brotli version for real offsets.
Figure 3: Why an 8-byte overflow is enough. The buffer sits inside a structure full of pointers the decoder trusts.

Call path inside Node.js

# Simplified native call chain for an inbound Brotli body (vulnerable runtime)
#0  DecodeBlockTypeAndLength()          // decode.c: impossible switch, returns FALSE
#1  DecodeLiteralBlockSwitch / Command / Distance ...
#2  ProcessCommands()                   // result treated as NEEDS_MORE_INPUT
#3  BrotliDecoderDecompressStream()     // appends next chunk into 8-byte buffer → OOB write
#4  BrotliDecoderContext::DoThreadPoolWork()
#5  CompressionStream<...>::Write()   // node_zlib.cc
#6  zlib.createBrotliDecompress()  ←  body-parser 2.x / Undici fetch() / user code

The fix

Upstream commit 0ed4268 ("Detect and report invalid stream earlier") landed in the public Brotli source tree on February 27, 2026. It makes DecodeBlockTypeAndLength() return a dedicated error, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, (numeric value -17) and propagates that error out of the decoder instead of collapsing it into "need more input". Genuine input shortfalls still return NEEDS_MORE_INPUT. Node.js cherry-picked the change and also exposed the new decoder error code to JavaScript. Critically, the commit was not flagged as a security fix, and no Brotli release shipped it until October 21. Any project vendoring Brotli from a release tarball stayed vulnerable for roughly eight months while the fix sat in plain sight in the public repository, a textbook "silent fix" window for anyone diffing upstream.

// Conceptual diff (not the literal patch)
- if (!SafeReadBlockType(s, br, &block_type)) return BROTLI_FALSE;   // ambiguous
+ if (s->num_block_types[tree_type] < 2)
+     return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH);
+ /* ...callers now distinguish a format error from NEEDS_MORE_INPUT
+    and move the decoder into a terminal error state */

Exploit Maturity Assessment

Exploit Status

  • No known exploit
  • Theoretical
  • PoC Available (public)
  • Functional Exploit (public RCE by dimasma0305)
  • Weaponized (in the wild): first exploitation attempt recorded October 26, 2026; added to CISA KEV October 27, 2026
AttributeAssessment
Exploit AvailabilityPublic: full RCE exploit released by researcher dimasma0305
Source of ExploitPublic code repository (dimasma0305); independent private research chains
DoS ReliabilityHigh: deterministic worker crash on any reachable decoder
RCE ReliabilityMedium: reliable against the exact build it targets (Node.js 24.22.0), but each build needs its own memory offset
Every Node.js build needs its own offset. Independent testing found that the offset that gave a shell on 24.22.0 produced nothing in 10,000 attempts against 22.23.3 and 26.12.0. That is not protection: attackers measure the offset once per popular build and reuse it against every host running that build. Supervisors such as PM2, systemd, Kubernetes and ECS restart crashed workers, and each restart hands the attacker a fresh heap layout to try.

Proof of Concept (PoC)

ItemDetail
Publicly AvailableYes: a public remote code execution exploit is available
PoC SourcePublic exploit repository by researcher dimasma0305 (created October 22, 2026). Review any third-party exploit code in an isolated lab before running it.
PurposeDeliver a crafted Brotli body that triggers the block-switch overflow, pivots execution and spawns a shell inside the Node.js process.
PrerequisitesVulnerable Node.js runtime (the public chain targets the official Node.js 24.22.0 x86-64 binary); a path that decodes attacker-controlled Brotli; knowledge of the accepted socket file descriptor, which is predictable on a quiet service.
Expected OutcomeVulnerable 24.22.0: shell running as the service account, wired to the attacker's own connection. Patched 24.22.1: request rejected with HTTP 400 and error ERR__ERROR_FORMAT_BLOCK_SWITCH; process keeps running.

Independently validated targets

TargetPath into decoderNode.js 24.22.0Node.js 24.22.1
Plain node:http serverApplication code calling node:zlib Brotli functionsShell (x86-64)Rejected
Official node:24.22.0-slim image as rootRestart policy enabledRoot shell (x86-64)Rejected
Express 5.0.0–5.3.0body-parser 2.x json() / raw()Code ran (arm64 and x86-64)Rejected, server stayed up
Koa 3@koa/bodyparser 6Shell (x86-64)Rejected
Outbound HTTP clientBuilt-in fetch() to attacker serverCode ran (arm64)Rejected
special conditon: a bare node:http server does not inflate Brotli bodies on its own, but many plain servers call zlib.createBrotliDecompress() themselves. Any such call on client data is exploitable without a framework.

PoC Reproducibility

Can a PoC be built from available information? Yes. A complete public exploit exists for Node.js 24.22.0 x86-64.

  • Required tools: an isolated lab with the official vulnerable node:24.22.0 image, the public exploit, a Brotli bitstream generator, and an ASan-instrumented Brotli build for root-cause triage.
  • High-level steps (lab only): declare a single block type for one category; let that block's counter run out mid-stream; force a block switch; keep streaming chunks so each append extends the overflow; observe the fault or shell on the vulnerable build and a clean -17 format error on the patched build.
  • Porting limits: official Node.js binaries are not position-independent, so code addresses are fixed per build, but the heap, stacks and allocator arenas are randomized. The exploit needs a build-specific offset and the correct socket descriptor, which becomes less predictable on busy services. Expect offsets for other popular builds to appear quickly.

Time-to-Exploit Analysis

IntervalDurationNotes
Upstream silent fix → public release~236 days (Feb 27 → Oct 21)Fix was diffable the whole time; attackers watching upstream had a long head start
Disclosure → public PoC release1 day (Oct 21 → Oct 22)Public RCE exploit by dimasma0305
Disclosure → RCE demonstrated on official build1 day (Oct 21 → Oct 22)arm64 on Oct 22; x86-64 reproduced by Oct 25
Disclosure → exposure scanning~4 days (by Oct 25)node2shell-check scanner mapping Brotli-decoding hosts
Patch → exploit in the wild5 days (Oct 21 → Oct 26, 15:08 UTC)First exploitation attempt against Node.js 24.22.0 x86-64
Disclosure → CISA KEV listing6 days (Oct 21 → Oct 27)CISA cites evidence of active exploitation; federal remediation due date November 10, 2026
Risk InterpretationImmediate (0–2 days): a public exploit exists, scanning has started, exploitation attempts are being recorded and CISA has listed the CVE as known exploited.

Vulnerability Timeline

Feb 27 Fix lands in Brotli source (not marked security) ~8-month silent-fix window: fix public, no release Oct 21 Node.js + Brotli releases CVE published Oct 22 Public RCE exploit; PyPI fixes Oct 23 Bun & Envoy advisories Oct 25 .NET advisory Oct 26 First ITW exploit attempt Oct 27 CISA KEV listing Not to scale; the February–October gap is compressed.
Figure 4: CVE-2026-111497 timeline, from the silent upstream fix to the CISA KEV listing.
EventDate
Vulnerable code ships in Brotli 1.0.3March 2018
Upstream fix committed to the Brotli source (commit 0ed4268, not labeled as security)February 27, 2026
Vulnerability discovered and reported (Khai Tran and Sylvie Mayer, OpenAI Codex Security)Not publicly disclosed
Public disclosure: Node.js October 2026 security release, Brotli 1.0.10 / 1.1.1 / 1.2.1October 21, 2026
Patch released: Node.js 22.23.4, 24.22.1, 26.12.1October 21, 2026
Public RCE exploit released by dimasma0305; RCE shown on official arm64 buildOctober 22, 2026
Python Brotli 1.2.1 and brotlicffi 1.2.1.0 published (PYSEC-2026-4322 / GHSA-9v8h-qqgm-4m35; PYSEC-2026-4323)October 22, 2026
Bun (GHSA-663w-cv8g-jj2p) and Envoy (GHSA-3wvj-j6qm-62mr) advisoriesOctober 23, 2026
Exposure scanner node2shell-check observed probing Brotli decode pathsOctober 25, 2026
Microsoft .NET advisoryOctober 25, 2026
Exploitation in the wild began (first recorded attempt, 15:08 UTC)October 26, 2026
CISA adds CVE-2026-111497 to the Known Exploited Vulnerabilities (KEV) catalog; federal due date November 10, 2026October 27, 2026

Affected and Fixed Versions Across the Ecosystem

Because Brotli is a library, this is a supply-chain-shaped vulnerability. Patching Node.js does not fix the Python worker next to it, the Envoy sidecar in front of it, or a curl binary in the same image.

ComponentVulnerableFixed InNotes
Node.js 26.x (Current)< 26.12.126.12.1Statically bundled; OS libbrotli updates do not help
Node.js 24.x (Active LTS)< 24.22.124.22.1Line targeted by the public exploit
Node.js 22.x (Maintenance LTS)< 22.23.422.23.4
Node.js 20.x, 23.x, 25.xAllNone (EoL)Migrate to a supported line; commercial EoL support is the only patch route
Google Brotli (libbrotli)1.0.3–1.0.9, 1.1.0, 1.2.01.0.10, 1.1.1, 1.2.1Distros may backport without a version bump; follow distro advisories
Python Brotli≤ 1.2.0, including older releases such as 0.5.2, 0.6.0 and 1.0.11.2.1Advisories: PYSEC-2026-4322 (lists these older releases as well as the 1.0.3 to 1.2.0 range), GHSA-9v8h-qqgm-4m35. urllib3, Requests, HTTPX, aiohttp and Scrapy auto-decode br when installed
Python brotlicffi≤ 1.2.0.2, including older releases such as 0.8.01.2.1.0Advisory: PYSEC-2026-4323 (separate PyPA advisory from the Brotli package; lists the older releases as well)
Ruby brotli gemEarlier releases0.9.1, 0.9.2Streaming decoder hangs instead of crashing
Debian / Alpine brotliDistro buildsbookworm 1.0.9-2+deb12u1 (DLA-4855-1); trixie 1.1.0-2+deb13u1 (DSA-6582-1); Alpine 1.2.1-r0 / 1.1.1-r0Matters where Node.js links the system library
Bun1.0.21 to < 1.4.41.4.4Rebuild bun build --compile executables
EnvoyWith Brotli decompressor configured1.40.0, 1.39.3, 1.38.6, 1.37.8, 1.36.12Proxy-layer exposure
.NET8, 9, 10 runtimes; 11 previews8.0.33, 9.0.22, 10.0.14, 11.0.0-rc.3Only via opt-in br request decompression, HttpClient auto-decompression or BrotliStream; .NET Framework unaffected
curl and othersBuilds linked to affected libbrotliVendor-specific--compressed decodes br
The same Node.js security release fixed nine other CVEs, including three High issues in DNS resolution and TLS hostname verification (CVE-2026-105482, CVE-2026-105483, CVE-2026-106069). Plan the upgrade as a whole release, not a single-CVE hotfix.

Active Exploitation & Threat Actors

QuestionStatus
Exploitation observed in the wildYes: first recorded attempt on October 26, 2026 at 15:08 UTC.
CISA KEV statusListed: added to the Known Exploited Vulnerabilities catalog on October 27, 2026 based on evidence of active exploitation. Federal civilian agencies must remediate by November 10, 2026. Known ransomware campaign use: Unknown.
Associated threat actorsNo named APT or ransomware group. The source IP (94.154.43.7) has a history of mass exploitation, including PHP2Shell exploitation, Palo Alto GlobalProtect and FortiOS SSL VPN probing, and Kubernetes API scanning, consistent with an opportunistic mass-exploitation operator.
ReconnaissanceA public exposure checker identifying as node2shell-check/1.0 (AS44620, registered in Turkey) sent 20 Brotli POST bodies in two bursts to map which hosts decompress Brotli.
Initial access methodSingle Brotli-encoded HTTP POST body of 23,069,983 bytes aimed at Node.js 24.22.0 x86-64
Payload deliveryDelivery stage copied from public exploit code (first 23,069,727 bytes reproduce byte for byte); original ROP chain that plants /bin/sh in its own data and calls Node's execvp
Outcome of observed attemptChain pivoted and crashed the worker but did not reach execvp in lab replay (0 of 50). Treat it as a first-generation attempt; the next iteration will likely work.
PersistenceNone observed; the chain wires stdin only, so follow-on actions (reverse shell, downloader) would happen in a second stage
Target sectors / geographyNot targeted; opportunistic, internet-wide

Post-Exploitation Details

AspectAssessment
Execution contextWhatever user runs node. In many containers that is root because images do not drop to USER node.
Privilege EscalationPossible, not inherent: root containers, mounted Docker sockets, privileged pods and over-scoped service accounts turn app RCE into host or cluster compromise
Lateral MovementLikely via secrets in process.env (DB URLs, cloud keys, JWT signing keys) and the cloud metadata service (169.254.169.254)
PersistenceModified node_modules or startup scripts on writable volumes, cron/systemd units on VMs, malicious images pushed with stolen registry credentials
Data ExfiltrationPossible; memory-disclosure variant can leak in-process secrets even without full RCE
Crash side effectFailed attempts crash the worker. On hosts where systemd-coredump is active, each forced crash writes a core file under /var/lib/systemd/coredump containing other users' session cookies, tokens and keys. Restrict and expire core files.
Behavior to expectnode spawning sh/bash/curl/wget, new outbound connections from the runtime, reverse shells, miner downloads, credential harvesting from env and mounted secrets

MITRE ATT&CK Mapping

TacticTechniqueIDNode2Shell Context
ReconnaissanceActive Scanning: Vulnerability ScanningT1595.002Fingerprinting Express/Node.js services (headers, error pages) and probing whether Content-Encoding: br bodies are accepted
Resource DevelopmentAcquire Infrastructure: ServerT1583.004Standing up a malicious server that returns crafted Brotli responses for the fetch() path
Initial AccessExploit Public-Facing ApplicationT1190Crafted Brotli request body to Express 5 / body-parser 2 endpoints
Initial AccessTrusted Relationship / Supply ChainT1199Compromised upstream API or partner webhook returning malicious Brotli to services that trust it
ExecutionExploitation for Client ExecutionT1203Server-side outbound fetch() decoding an attacker response (the Node process acts as the "client")
ExecutionCommand and Scripting Interpreter: Unix ShellT1059.004Shellcode pivoting to /bin/sh after control-flow hijack
PersistenceServer Software Component: Web ShellT1505.003Injecting a route or middleware into application code on writable volumes
Privilege EscalationEscape to HostT1611From root-in-container with privileged mounts or Docker socket
Defense EvasionImpair Defenses / Indicator RemovalT1070Crash-and-restart loops look like ordinary instability; orchestrators recycle containers and their logs
Credential AccessUnsecured Credentials: Cloud Instance Metadata APIT1552.005Pulling IAM credentials from IMDS from inside the compromised process
Credential AccessUnsecured Credentials: Credentials In FilesT1552.001Reading .env, mounted Kubernetes secrets, ~/.npmrc tokens
Command & ControlApplication Layer Protocol: Web ProtocolsT1071.001HTTPS beacons blending with the service's normal outbound API traffic
ImpactEndpoint Denial of Service: Application or System ExploitationT1499.004Public PoC crashes the Node.js process; repeatable per request
ImpactResource HijackingT1496Cryptominer deployment, the most common monetization of runtime RCE

Vulnerability Chaining Opportunities

Can this be chained? Yes. Node2Shell is an excellent second stage for anything that lets an attacker choose a URL your server fetches.

1. URL input webhook / preview / import-from-URL 2. fetch() server calls attacker host 3. Crafted br response → OOB write → RCE 4. IMDS / env steal cloud keys and DB secrets 5. Impact cloud account takeover, data theft, miners No authentication required at any step if the URL feature is public. SSRF allow-lists that only block internal IPs do not help here.
Figure 5: The outbound chain. A "harmless" URL-fetch feature becomes an RCE delivery mechanism.
ChainEnablerOutcome
Open URL-fetch features + Node2ShellWebhooks, OAuth/OIDC discovery, oEmbed, link unfurling, image proxiesUnauthenticated RCE through an outbound request
Next.js 15/16 apps (CVE-2026-96889 and others patched Oct 14) + Node2ShellUnpatched framework and unpatched runtime in the same imageMultiple independent RCE paths in one deployment
Node2Shell + root containerMissing USER directive, privileged pod, Docker socket mountHost or node takeover
Node2Shell + exposed instance metadataIMDSv1 left enabled (no session token requirement), or a metadata endpoint that containers can reach with no network restrictionCloud credential theft and account pivot
Memory disclosure variant + session secretsSecrets in process memoryToken forgery without needing reliable RCE

Patch / Mitigation

ItemDetail
Patch AvailableYes
Patch VersionsNode.js 22.23.4, 24.22.1, 26.12.1 (or later in each line)
DownloadNode.js October 2026 Security Releases  |  Google Brotli 1.0.10 / 1.1.1 / 1.2.1
NotesRestart is mandatory: running processes keep the old decoder in memory. Rebuild every container image, including multi-stage builds that copy a Node binary from an older base. Node.js states there is no workaround; everything below is a stopgap.

Verify the runtime you actually run

# Host / VM
node --version
# Do NOT rely on process.versions.brotli: the fix was cherry-picked without a version bump,
# so it reports 1.2.0 on both patched and vulnerable builds. Check the Node.js release instead.

# Patched runtimes expose the new decoder error constant (expect a number, not undefined)
node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH"

# Every image in your registry / cluster, not just the app server
docker run --rm --entrypoint node <image> --version
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\t"}{.spec.containers[*].image}{"\n"}{end}'

# Python environments that silently enable br decoding
pip show brotli brotlicffi 2>/dev/null | grep -E "Name|Version"

Interim mitigation while rolling out

# NGINX: refuse Brotli-encoded request bodies at the edge
if ($http_content_encoding ~* "br") { return 415; }

# HAProxy
http-request deny deny_status 415 if { req.hdr(content-encoding) -i -m sub br }
// Express 5: reject br bodies BEFORE any body-parser middleware runs
app.use((req, res, next) => {
  const ce = (req.headers['content-encoding'] || '').toLowerCase();
  if (ce.split(',').some(v => v.trim() === 'br')) {
    return res.status(415).send('Unsupported Content-Encoding');
  }
  next();
});
app.use(express.json());
Outbound responses are the hard part. Edge filters only see inbound requests. Sending Accept-Encoding: gzip from fetch() does not protect you: a hostile server can return Content-Encoding: br anyway and the client will decode it. Until patched, route untrusted outbound fetches through an egress proxy that strips or rejects br responses, or temporarily disable the URL-fetching features.

Indicators of Compromise (IOCs)

IOCs Available: Yes. Indicators below come from the first recorded in-the-wild exploitation attempt and the reconnaissance that preceded it. The exploit body varies per target, so behavioral indicators remain essential.

Network and payload indicators

TypeIndicatorContext
IP address94.154.43.7Exploitation source, Oct 26, 2026 15:08 UTC. Rated malicious with high confidence; first seen Dec 15, 2025; active on 47 of the prior 90 days
SHA-256 (request body)7fcbdaaecd4419e2930927844f3b772668204737a4e7602a2290c991c93c5c4bBrotli-encoded exploit body
Payload size23,069,983 bytesSingle POST body with Content-Encoding: br
Payload structure1031-byte memory-layout stage + 256-byte displacement stageDelivery copied from public exploit; ROP chain original
User-AgentMozilla/5.0 (compatible; node2shell-check/1.0)Public exposure checker; source network AS44620 (Turkey)
Scan patternGET / then 20 POST bodies with Content-Encoding: br in two bursts ~2 minutes apart5 malformed 1032-byte bodies padded with repeated A; 15 valid Brotli bodies in four sizes, largest 65,743 bytes
TargetOfficial Node.js v24.22.0 Linux x86-64 release binaryChain addresses are specific to this build
# Structured IOC list (CVE-2026-111497 / Node2Shell)
ipv4        94.154.43.7                                                   # exploitation
sha256      7fcbdaaecd4419e2930927844f3b772668204737a4e7602a2290c991c93c5c4b   # br exploit body
asn         AS44620                                                       # node2shell-check scanner source
user_agent  Mozilla/5.0 (compatible; node2shell-check/1.0)
http        POST, Content-Encoding: br, Content-Length: 23069983
http        POST, Content-Encoding: br, Content-Length: 1032 (A-padded)

Host and behavioral indicators

Indicator TypeWhat to look forConfidence
HTTP requestVery large (multi-MB) Content-Encoding: br POST bodies to API endpoints; browsers almost never send br request bodiesHigh
HTTP response (patched)HTTP 400 with ERR__ERROR_FORMAT_BLOCK_SWITCH: an exploit attempt blocked by a patched runtimeHigh (attempt)
Processnode exits with SIGSEGV / SIGBUS / SIGABRT (exit codes 139, 135, 134); container restart loopsHigh (attempt)
Process treenode → sh/bash/dash/curl/wget/python/ncHigh (post-exploitation)
Host networkThe node process opening connections to 169.254.169.254, mining pools or new IPs after a crash and restart (needs host or eBPF telemetry)High
FilesystemNew files in /tmp, /dev/shm; modified files under node_modules; new core files in /var/lib/systemd/coredumpHigh

Detection & Monitoring

Log sources

Log SourceKey DataDefault?Detects
Reverse proxy / WAF / LB logsRequest Content-Encoding header, status, bytesPartial: header often not logged; add it to log formatInbound exploit attempts
Container runtime / Kubernetes eventsOOMKilled vs Error, exit codes, restart countsYesCrash loops from DoS or failed RCE attempts
systemd journal / PM2 logsSignals, core dumpsYesCrashes on VMs
Falco / Tetragon / auditd / Sysmon for Linuxexecve, connect, file writesNoShell spawn and C2 after successful exploitation
Egress proxy logsResponse Content-Encoding to internal servicesNoMalicious Brotli responses on the fetch() path
VPC flow logsConnections between workloads and the internetPartialC2 and exfiltration. Not IMDS access: AWS excludes traffic to and from 169.254.169.254 from VPC Flow Logs, so use host-level telemetry (eBPF, auditd, EDR) for metadata access

Splunk

# ── QUERY 1: Brotli-encoded request bodies to app endpoints ─────────────
index=web sourcetype IN ("nginx:plus:kv","nginx:access","aws:elb:accesslogs")
  http_method IN ("POST","PUT","PATCH")
  http_content_encoding="*br*"
| stats count values(uri_path) AS paths values(status) AS statuses max(bytes_in) AS max_body by src_ip
| where count > 3 OR max_body > 10000000
| eval risk="HIGH - Possible CVE-2026-111497 probing / exploitation"

# ── QUERY 2: Node.js crash bursts per host ──────────────────────────────
# Aggregate with stats by time bucket AND host, filter on the plain count field,
# then chart if needed. (timechart ... by host pivots hosts into separate columns,
# so a later "where count > 2" has no count field to test.)
index=k8s OR index=linux
  (process_name="node" OR container_command="*node*")
  (exit_code IN (134,135,139) OR signal IN ("SIGSEGV","SIGBUS","SIGABRT"))
| bin _time span=5m
| stats count AS crashes by _time host
| where crashes > 2
| xyseries _time host crashes            ``` optional: chart only the filtered rows ```

# ── QUERY 3: Node.js spawning a shell or downloader (post-RCE) ──────────
index=edr (parent_process_name="node" OR parent_process_name="nodejs")
  process_name IN ("sh","bash","dash","curl","wget","nc","ncat","python","python3","perl")
| eval risk="CRITICAL - Node.js child shell"
| table _time host user parent_process_name process_name process_cmdline risk

# ── QUERY 4: br request FOLLOWED BY a crash on the same host (≤ 60 s) ──
# Ordered correlation: request must precede the crash. src_ip identifies the
# client that sent the preceding request; it is a candidate, not proof of
# attribution, unless request-level IDs (e.g. X-Request-ID) link the two events.
(index=web http_content_encoding="*br*" http_method="POST")
OR (index=k8s exit_code IN (134,135,139))
| eval etype=if(index="web","req","crash")
| eval req_time=if(etype="req",_time,null()), req_src=if(etype="req",src_ip,null())
| sort 0 host _time
| streamstats last(req_time) AS last_req_time last(req_src) AS last_req_src by host
| where etype="crash" AND isnotnull(last_req_time) AND (_time - last_req_time) >= 0 AND (_time - last_req_time) <= 60
| eval gap_s=_time - last_req_time
| table _time host last_req_src gap_s
| rename last_req_src AS "preceding br request src (candidate)"

# ── QUERY 5: Known IOCs ─────────────────────────────────────────────────
index=web (src_ip="94.154.43.7" OR http_user_agent="*node2shell-check*")
| stats count earliest(_time) AS first latest(_time) AS last values(uri_path) by src_ip http_user_agent
Query 4 assumes web logs and crash events share a host value (for example, the pod or instance name behind the load balancer). If the proxy log only records the load balancer, correlate on the upstream address field instead, and attribute a source only when request-level evidence links the request to the crashing worker.

Elastic / KQL

# ── br request bodies ────────────────────────────────────────────────────
http.request.method:(POST or PUT or PATCH) and
http.request.headers.content-encoding:*br*

# ── node spawning shells ─────────────────────────────────────────────────
event.category:process and event.type:start and
process.parent.name:(node or nodejs) and
process.name:(sh or bash or dash or curl or wget or nc or python3)

# ── node talking to instance metadata (endpoint telemetry, not flow logs) ─
event.category:network and process.name:node and destination.ip:"169.254.169.254"

# ── Known IOCs ───────────────────────────────────────────────────────────
source.ip:"94.154.43.7" or user_agent.original:*node2shell-check*

Microsoft Sentinel (KQL)

// ── Linux endpoint (MDE): node spawning a shell ─────────────────────────
DeviceProcessEvents
| where InitiatingProcessFileName in~ ("node", "nodejs")
| where FileName in~ ("sh","bash","dash","curl","wget","nc","python3","perl")
| project Timestamp, DeviceName, AccountName, InitiatingProcessCommandLine, ProcessCommandLine
| extend Risk = "CRITICAL - Possible Node2Shell post-exploitation"

// ── Container crash loops (Container Insights) ──────────────────────────
KubePodInventory
| where ContainerRestartCount > 3
| where ContainerLastStatus has_any ("Error", "139", "134")
| summarize Restarts = max(ContainerRestartCount) by Name, Namespace, bin(TimeGenerated, 15m)

// ── Patched runtime rejecting exploit input (app logs) ──────────────────
AppTraces
| where Message has "ERR__ERROR_FORMAT_BLOCK_SWITCH"
| summarize Attempts = count() by AppRoleName, bin(TimeGenerated, 15m)

// ── App Gateway / Front Door: br-encoded request bodies ─────────────────
AzureDiagnostics
| where Category in ("ApplicationGatewayAccessLog", "FrontDoorAccessLog")
| where requestHeaders_s has "content-encoding: br"
| summarize count() by clientIP_s, requestUri_s, bin(TimeGenerated, 10m)

Falco (runtime)

- rule: Node.js Spawns Shell (Node2Shell post-exploitation)
  desc: A Node.js process started a shell or network utility
  condition: >
    spawned_process and proc.pname in (node, nodejs) and
    proc.name in (sh, bash, dash, curl, wget, nc, ncat, python3, perl)
  output: "node child process (parent=%proc.pname cmd=%proc.cmdline container=%container.name image=%container.image.repository)"
  priority: CRITICAL
  tags: [CVE-2026-111497, T1059.004]

Suricata (inbound, cleartext or after TLS termination)

alert http any any -> $HOME_NET any (msg:"Possible CVE-2026-111497 Node2Shell - Brotli request body";
  flow:established,to_server; http.method; content:"POST";
  http.header; content:"Content-Encoding|3a 20|br"; nocase;
  threshold:type both, track by_src, count 5, seconds 60;
  classtype:attempted-admin; sid:9261114; rev:1;)
These rules are hunting baselines, not signatures for the malformed bitstream. Tune them against legitimate clients (some SDKs and mobile apps do compress uploads) before alerting on them in production.

Behavioral indicators for EDR

  • Any interactive shell whose ancestor is node in a container that normally never executes child processes.
  • Memory pages becoming writable and executable in a node process outside V8's JIT regions.
  • A node process that crashed and restarted several times, then begins outbound connections it never made before.
  • Reads of /proc/self/environ, /var/run/secrets/kubernetes.io/serviceaccount/token or ~/.aws/credentials by the runtime.

Risk Assessment

Impact
Critical
Reachability
Conditional
Exploit maturity
Public RCE + ITW
Likelihood (30 days)
Very High
Overall
Critical
FactorRatingRationale
ImpactCriticalFull code execution in the server process; confidentiality, integrity and availability all high
LikelihoodVery HighPublic RCE exploit, active exposure scanning, in-the-wild exploitation attempts and a CISA KEV listing; Node.js is everywhere and versions leak through headers and image tags
Exploit maturity influenceRaises priority sharplyPublic exploit within a day of disclosure, in-the-wild attempts within five days and a CISA KEV listing within six; per-build offsets enable version-targeted waves
Mitigating factorsLimitedNot every app decodes untrusted Brotli; each build needs its own offset; the first observed in-the-wild chain failed
Overall Risk LevelCritical for exposed paths; High otherwisePatch within 24–72 hours for internet-facing Express 5 services and any service fetching attacker-influenced URLs

Exposure decision flow

Node.js below fixed version? Not affected (check other Brotli copies) No Decodes br from untrusted source? Yes HIGH: patch in normal emergency cycle (≤ 7 d) No Internet-reachable or fetches user URLs? Yes HIGH: internal attack path; patch ≤ 72 h No CRITICAL: patch within 24 h Block br at edge, restrict outbound fetch, enable runtime detection, hunt for crashes and node child processes since Oct 21 Yes "Decodes br from untrusted source" includes: • Express 5 / body-parser 2 json() or raw() • fetch()/Undici to third-party or user URLs • zlib.brotliDecompress on uploads or imports • Proxies/SSR that relay upstream responses • Python/Bun/.NET sidecars with Brotli enabled A bare node:http server alone does not qualify.
Figure 6: Prioritization flow for Node2Shell exposure.

Remediation Recommendations

P1: Upgrade every Node.js runtime and restartIMMEDIATE
Move to 22.23.4, 24.22.1 or 26.12.1 (or later). Rebuild container images from refreshed node: base tags, pin by digest, redeploy and restart. Include CI runners, serverless layers, Electron-free build tools and SSR workers. Confirm with node --version inside the running container, not the Dockerfile. The CISA KEV federal due date of November 10, 2026 is an outer limit for agencies, not a target; internet-facing services should be patched well before it.
P1: Find every other copy of BrotliIMMEDIATE
Update Python Brotli to 1.2.1 and brotlicffi to 1.2.1.0 (regenerate lockfiles). Treat every installed version at or below 1.2.0 (Brotli) and 1.2.0.2 (brotlicffi) as affected, including old pinned 0.x and 1.0.x releases, Bun to 1.4.4, Envoy to a fixed release, .NET runtimes to 8.0.33 / 9.0.22 / 10.0.14, and the OS libbrotli package per your distribution's advisory. Use SBOMs (Syft, Trivy) to locate vendored copies inside images.
P1: Block Brotli request bodies where patching lagsIMMEDIATE
Reject Content-Encoding: br on inbound requests at the WAF, load balancer or reverse proxy, or with an Express middleware mounted before body parsers. Few legitimate browser clients send compressed request bodies, so the business impact is usually small.
P1: Constrain outbound fetches to untrusted hostsIMMEDIATE
Pause or proxy features that fetch user-supplied URLs. Force such traffic through an egress proxy that rejects br responses until runtimes are patched. Remember that request headers you send do not control what a hostile server returns.
P1: Move EoL Node.js (20.x, 23.x, 25.x) off productionIMMEDIATE
No upstream patch will ever ship for these lines. Upgrade to 22.x or 24.x LTS, or put them behind compensating controls with a dated decommission plan.
P2: Run Node.js as non-root with least privilegeHIGH
Use USER node, read-only root filesystems, allowPrivilegeEscalation: false, dropped capabilities, seccomp RuntimeDefault, and no Docker socket mounts. These turn a successful RCE into a contained incident.
P2: Protect cloud credentials from the app processHIGH
Require IMDSv2 session tokens and set the metadata response hop limit to match how Node.js runs. A hop limit of 1 suits Node.js running directly on the instance, but on EC2 instances that host containers AWS recommends a hop limit of 2, because a limit of 1 stops containerized workloads from retrieving IMDSv2 tokens. Where you use 2, block pod and container access to 169.254.169.254 with network policy or host firewall rules for workloads that do not need it, use workload identity with narrowly scoped roles, and keep long-lived secrets out of environment variables where possible.
P2: Deploy runtime detection for node child processesHIGH
Most Node.js services never spawn shells. Alerting on node → sh is cheap, high-signal and catches this and every future runtime RCE. Treat repeated node segfaults as a security event, not just an SRE ticket.
P3: Track upstream "silent fixes" in vendored native codePLANNED
The Brotli fix sat in public source for eight months. Feed dependency-bump PRs and upstream commit monitoring for native libraries (Brotli, zlib, OpenSSL, c-ares, llhttp) into your vulnerability management process, not just CVE feeds.
P3: Inventory runtimes, not just packagesPLANNED
Package managers miss Node binaries copied into images, bundled in desktop apps or shipped in vendor appliances. Maintain an inventory of runtime versions per host and per image, with a 24–72 hour SLA for pre-auth RCE in language runtimes.

SecPod Coverage

CapabilityCVE-2026-111497 Coverage
Vulnerability DetectionOVAL-based checks identifying vulnerable Node.js 22.x, 24.x and 26.x installations on Windows, macOS and Linux, plus detection of end-of-life Node.js lines that will not receive a fix
Patch AssessmentSaner flags endpoints and servers running Node.js below 22.23.4, 24.22.1 or 26.12.1 as critical-priority remediation
RemediationPatch deployment for supported Node.js installations via Saner; EoL runtime migration advisory workflow

Key Takeaways

  • The bug is in the runtime, not your code. Express, body-parser and fetch() are delivery routes into a native decoder compiled into Node.js.
  • Outbound traffic is attack surface. A server that fetches a URL an attacker controls can be exploited through the response, and edge WAFs never see it.
  • One error code, two meanings. Collapsing "malformed" into "incomplete" is a recurring parser bug class; streaming decoders are especially prone to it.
  • This is now exploited. A public exploit, in-the-wild attempts and a CISA KEV listing mean the window for routine patching has closed.
  • Silent fixes are an attacker head start. Eight months between the upstream commit and a release is time defenders did not know they had lost.
  • Patch, rebuild, restart, verify. Updating a package without restarting the process or rebuilding the image leaves the vulnerable decoder running.

Node2Shell is a reminder that the lowest layers of the web stack, compression, parsing and TLS, are now being mined with AI-assisted tooling at a pace that compresses the gap between disclosure and a public exploit to about a day, and to in-the-wild attempts and a CISA KEV listing within a week. The patch is available and drop-in. The fastest way to remove this risk is to replace every vulnerable Node.js runtime you run and restart it today.

Instantly Fix Risks with Saner Patch Management

Saner patch management is a continuous, automated, and integrated software that helps security teams fix risks exploited in the wild. The software supports major operating systems such as Windows, Linux, and macOS, along with 550+ third-party applications.

It allows teams to set up a safe testing area to validate patches before deploying them in production. Saner patch management also supports patch rollback in case of patch failure or system malfunction.

Experience the fastest and most accurate patching software here.

SecPod Technologies Vulnerability Research  |  CVE-2026-111497 (Node2Shell)  |  Published October 28, 2026

Featured Posts

Open PHP2Shell After the Patch: The FAQ for Verification, Detection and Response
PHP2Shell After the Patch: The FAQ for Verification, Detection and Response

CVE Research

PHP2Shell After the Patch: The FAQ for Verification, Detection and Response

Patches for PHP2Shell are out, but verifying them, trusting interim mitigations and checking for earlier compromise are where teams are stuck. This FAQ covers stale version banners, container rebuilds, WAF limits, detection signals and what to rotate.

Oct 26, 2026

Open AI-Powered Ransomware Intrusions: Inside the Phantom Framework Linked to INC and The Gentlemen
AI-Powered Ransomware Intrusions: Inside the Phantom Framework Linked to INC and The Gentlemen

CVE Research

AI-Powered Ransomware Intrusions: Inside the Phantom Framework Linked to INC and The Gentlemen

The Phantom Framework supports AI-assisted cyber intrusions by enabling lateral movement, persistence, data exfiltration, and progression toward ransomware deployment, putting enterprise networks and sensitive organizational data at risk.

Oct 26, 2026

Open Tailscale SSH Flaws Turn a Single Foothold Into Root Access on Target Servers
vulnerability researchTailscale SSH Flaws Turn a Single Foothold Into Root Access on Target Servers

CVE Research

Tailscale SSH Flaws Turn a Single Foothold Into Root Access on Target Servers

Tailworm reports high-severity Tailscale SSH issues on version 1.104.2 where Unix-socket forwarding can run as root inside tailscaled, including TOCTOU races on path checks. A policy-approved device may escalate on reachable hosts. Tailscale is reviewing the report and working on fixes. Interim steps center on tighter SSH policy and disabling forwarding.

Oct 26, 2026

Open ARTEX and the South Korean Bank Attacks: Why Removing an AI Penetration Testing Tool Is Not Enough
ARTEX and the South Korean Bank Attacks: Why Removing an AI Penetration Testing Tool Is Not Enough

CVE Research

ARTEX and the South Korean Bank Attacks: Why Removing an AI Penetration Testing Tool Is Not Enough

Oct 23, 2026