Brotli Node2Shell KQL Hunting (CVE-2026-111497) | SecurityHQ
CVE-2026-111497 | Microsoft Sentinel KQL Threat Huntingix behavioral threat hunts for Microsoft Entra ID and Microsoft Sentinel
Threat Context
CVE-2026-111497 is an out-of-bounds write in the Brotli decoder. An application that decompresses attacker-controlled Brotli streams can suffer memory corruption, information disclosure, denial of service, or potentially remote code execution. Node.js is one affected runtime, but Brotli is also embedded in other products. Exposure depends on actual decompression of untrusted data; observing the HTTP br encoding token does not establish vulnerability or exploitation.
Node.js released fixes on 21 October 2026. Validate the applicable vendor advisory, runtime version, build provenance, and application decompression path before drawing an exposure conclusion. These hunts identify suspicious adjacent behavior; they are not CVE-specific signatures.
Status as of 26 October 2026: Rapid7 is not aware of in-the-wild exploitation, Akamai’s analysis reports none, and the CVE is not in the CISA Known Exploited Vulnerabilities catalog; researchers have demonstrated memory disclosure and command execution on unpatched Node.js. No public indicators of compromise or observed post-exploitation behavior have been published, so the host process and crash hunts below are behavioral hypotheses for a post-exploitation scenario, not detections of known attacker activity. Vendors track this issue as Node2Shell.
Fixed Node.js versions: 22.23.4, 24.22.1 and 26.12.1. End-of-life release lines (for example 20.x) receive no fix and should be treated as affected. Bun, .NET, Python Brotli packages, Envoy, Istio and operating-system packages that bundle the Brotli decoder are also in scope; see the vendor advisories under References.
Execution guidance and field mapping
These examples assume standard Microsoft Defender XDR hunting tables in the Sentinel workspace (DeviceProcessEvents, DeviceEvents) or conventional Sentinel Syslog/CommonSecurityLog. Confirm table existence, field population, connector routing, and retention before execution. Replace custom fields and source selection where necessary; no single normalized schema guarantees Brotli HTTP headers, application crashes, and child-process telemetry together.
Suggested hunting window: last 7 days for process activity, initially 24 hours for high-volume reverse proxy and WAF logs, and 14 days for low-volume diagnostic errors. Validate on narrower periods first. Queries are hunting starting points, not production-ready alert rules.
KQL hunting queries
Inbound Brotli-encoded requests and server error responses
Objective: Identify inbound HTTP requests to your applications whose request body is declared Brotli-encoded (Content-Encoding: br on the request), together with server-side error responses. Outbound proxy traffic and responses fetched from third parties are out of scope. A br header is normal for some clients and APIs, not an exploit indicator.
Required telemetry: CommonSecurityLog from the reverse proxy, WAF or load balancer in front of the applications, containing request headers in Message, RequestURL, AdditionalExtensions or other mapped fields, plus RequestMethod.
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where RequestMethod in~ ('POST','PUT','PATCH')
| extend Detail = strcat(tostring(Message), ' ', tostring(AdditionalExtensions))
| where Detail has 'content-encoding'
| where Detail matches regex @'(?i)content-encoding\s*[:=]\s*[^\r\n;]*\bbr\b'
| extend ResponseCode = toint(DeviceCustomNumber1)
| summarize Requests=count(),
ServerErrors=countif(ResponseCode between (500 .. 599)),
ClientErrors=countif(ResponseCode between (400 .. 499)),
FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated)
by SourceIP, DestinationHostName, RequestURL, DeviceVendor, DeviceProduct
| order by ServerErrors desc, Requests desc
Analyst validation: Only body-carrying methods are matched, because a compressed request body is the inbound path to a server-side decoder. Accept-Encoding is deliberately not matched: it only advertises client support and appears on most browser traffic. Confirm that your device logs request headers (not only response headers) and that its Content-Encoding field reflects the request; confirm the vendor-specific response code field, since DeviceCustomNumber1 is only an illustration. Prioritize sources sending Brotli-encoded bodies to endpoints that do not normally receive them, repeated attempts with 4xx/5xx responses, and any hit followed by crashes (Query 2) or child processes (Query 3) on the destination host. Validate against a baseline of legitimate API clients.
Application decoder and crash diagnostics
Objective: Look for crash and decompression-related strings from the relevant host/app diagnostic streams.
Required telemetry: Syslog logs from Linux application servers, or an equivalent mapped application-log table.
Syslog
| where TimeGenerated > ago(14d)
| extend Detail = strcat(tostring(SyslogMessage), ' ', tostring(ProcessName))
| where Detail matches regex @'(?i)(brotli|decompress|segfault|SIGSEGV|core dumped|fatal error)'
| where Detail matches regex @'(?i)(brotli|decoder|decompress|segfault|SIGSEGV|core dumped)'
or ProcessName in~ ('node','bun','dotnet','httpd','apache2','nginx')
or Detail matches regex @'(?i)\b(node|node\.js|httpd|nginx|dotnet)\b'
| project TimeGenerated, Computer, HostIP, ProcessName, SeverityLevel, SyslogMessage
| order by TimeGenerated desc
Analyst validation: Substring matching (contains) is used so that terms such as brotliDecompress and decompression are caught; the cost is extra noise, since “node” also matches words like “nodes”. Look for repeated failures near externally supplied compressed inputs. Generic segfault, Node.js or service failures have many benign causes; check crash dumps or application logs where permitted.
Web/application runtime launching command interpreters
Objective: Identify suspicious direct child processes of common server-side runtimes, including Node.js.
Required telemetry: Microsoft Defender XDR DeviceProcessEvents integrated into Sentinel.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName in~
('node','node.exe','bun','bun.exe','dotnet','dotnet.exe',
'httpd','httpd.exe','apache2','nginx','nginx.exe','w3wp.exe')
| where FileName in~
('sh','bash','dash','zsh','cmd.exe','powershell.exe','pwsh.exe',
'python','python3','perl','curl','curl.exe','wget','wget.exe',
'certutil.exe','nc','ncat','socat')
| project Timestamp, DeviceName, AccountName,
InitiatingProcessFileName, InitiatingProcessCommandLine,
FileName, ProcessCommandLine, ProcessId,
InitiatingProcessId, InitiatingProcessCreationTime
| order by Timestamp desc
Analyst validation: Confirm process-tree lineage, service identity and legitimate deployment tooling. A suspicious child process supports a post-exploitation hypothesis, but does not reveal which vulnerability created the foothold.
Reconnaissance executed by application workers
Objective: Surface host reconnaissance commands started directly by application workers.
Required telemetry: Microsoft Defender XDR DeviceProcessEvents.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName in~
('node','node.exe','bun','bun.exe','dotnet','dotnet.exe',
'httpd','httpd.exe','apache2','nginx','nginx.exe','w3wp.exe')
| where FileName in~
('whoami','whoami.exe','id','uname','hostname','hostname.exe',
'ipconfig.exe','systeminfo.exe','net.exe','nltest.exe')
| project Timestamp, DeviceName, DeviceId, AccountName,
InitiatingProcessFileName, FileName,
InitiatingProcessCommandLine, ProcessCommandLine,
InitiatingProcessId, ProcessId
| order by Timestamp desc
Analyst validation: Only direct children of application workers are matched, which keeps administrative noise low. Discovery commands run through an intermediate shell (worker, then sh/bash, then command) are not matched here; pivot from Query 3 results on DeviceName and InitiatingProcessId to find them rather than matching sh/bash as parents, which would match routine administration and cron activity. Confirm lineage and service identity before escalating.
Potential reverse-shell and payload retrieval commands
Objective: Identify network-capable commands started by application-related processes.
Required telemetry: Microsoft Defender XDR DeviceProcessEvents.
DeviceProcessEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName in~
('node','node.exe','bun','bun.exe','dotnet','dotnet.exe',
'httpd','httpd.exe','apache2','nginx','nginx.exe','w3wp.exe')
| where FileName in~
('sh','bash','curl','curl.exe','wget','wget.exe','nc','ncat',
'socat','powershell.exe','pwsh.exe','certutil.exe')
| where ProcessCommandLine has_any
('/dev/tcp','/dev/udp','mkfifo','DownloadString','Invoke-WebRequest','EncodedCommand')
or ProcessCommandLine matches regex @'(?i)(^|\s)-(e|enc)\s'
or (FileName in~ ('curl','curl.exe','wget','wget.exe','certutil.exe')
and ProcessCommandLine matches regex @'(?i)https?://')
| project Timestamp, DeviceName, AccountName,
InitiatingProcessFileName, FileName, ProcessCommandLine,
InitiatingProcessCommandLine, ProcessId, InitiatingProcessId
| order by Timestamp desc
Analyst validation: The -e/-enc switch is matched only as a standalone argument (for example nc -e or powershell -enc), and URLs are matched only for curl, wget and certutil. These are host-side post-exploitation checks on the application server, not proxy analysis. Any download by an application worker will still match the URL clause, so treat the output as a triage list: validate command-line semantics and destinations, and baseline known deployment and update tooling.
Repeated application process terminations
Objective: Highlight application process crashes or abnormal terminations recorded by the endpoint connector.
Required telemetry: Microsoft Defender XDR DeviceEvents; exact ActionType and AdditionalFields vary by sensor.
DeviceEvents
| where Timestamp > ago(14d)
| extend Details = tostring(AdditionalFields)
| where ActionType matches regex @'(?i)(Crash|Terminated|ProcessExit|ApplicationError)'
or Details has_any ('SIGSEGV','segfault','core dumped','brotli')
| where Details has_any ('node','bun','dotnet','brotli','httpd','nginx')
or InitiatingProcessFileName in~
('node','node.exe','bun','bun.exe','dotnet','dotnet.exe',
'httpd','httpd.exe','apache2','nginx','nginx.exe','w3wp.exe')
| summarize Events=count(), FirstSeen=min(Timestamp), LastSeen=max(Timestamp),
Actions=make_set(ActionType,20)
by DeviceName, InitiatingProcessFileName
| order by Events desc
Analyst validation: First confirm that your connector actually emits crash or termination actions; an empty result may mean missing telemetry. The ActionType values shown are illustrative, and Defender for Endpoint on Linux may not record crashes this way, so use Syslog, auditd or systemd-coredump data (Query 2) as the primary crash source on Linux servers. Correlate spikes with application logs and Brotli-decompression exposure; no process crash is mandatory for exploitation.
Investigation and escalation
Prioritize findings when (a) an affected application demonstrably decodes untrusted Brotli input, (b) inbound Brotli-encoded requests to it coincide with unusual crashes or decoder errors, and (c) host telemetry shows newly spawned shells or reconnaissance. Cross-check timelines, request metadata, process trees, EDR prevention outcomes and service ownership before escalation.
A clean hunt does not establish immunity: the decoder can corrupt memory without launching a child process, and headers/body content may be invisible to SIEM due to TLS termination or missing instrumentation. Threat hunting complements patch verification and exposure assessment; it is not a substitute.
Operational checklist
- Verify table availability, telemetry coverage, retention and field mapping before execution.
- Start with narrow time windows and filter to known exposed services or hosts.
- Baseline legitimate background jobs and common application child processes.
- Document findings, false positives, query changes and coverage gaps; escalate credible activity via SOC workflows.
- Verify affected runtimes and library versions against vendor advisories (Node.js 22.23.4, 24.22.1 or 26.12.1 and later; end-of-life lines have no fix) and prioritize patching.
References
- Red Hat Security Bulletin RHSB-2026-015: https://access.redhat.com/security/vulnerabilities/RHSB-2026-015
- Microsoft .NET advisory: https://github.com/dotnet/runtime/security/advisories/GHSA-p3jp-9v83-76w9
- Akamai, Inside Node2Shell (26 October 2026): https://www.akamai.com/blog/security-research/inside-node2shell-closer-look-cve-2026-111497
- Bun security advisory: https://github.com/oven-sh/bun/security/advisories/GHSA-663w-cv8g-jj2p
- Rapid7, CVE-2026-111497: Critical out-of-bounds write in the Node.js Brotli decoder: https://www.rapid7.com/blog/post/etr-cve-2026-111497-critical-out-of-bounds-write-in-the-node-js-brotli-decoder/
- Vercel, Node.js security advisory: https://vercel.com/kb/bulletin/node-js-security-advisory