2026-10-27 Brotli / Node2Shell CVE-2026-111497 — Microsoft Sentinel Threat Hunts

Threat Overview

CVE-2026-111497 is an out-of-bounds write in the Google Brotli C decoder, not a vulnerability unique to HTTP/2 or Node.js. A malformed Brotli stream can make the decoder mishandle an invalid block switch and overwrite adjacent memory. An attacker must cause a vulnerable process to decompress attacker-controlled Brotli content. This may occur through a server that accepts compressed request bodies, an HTTP client that automatically decompresses an attacker-controlled response, or another application decoding untrusted data. The upstream advisory describes process crashes, memory disclosure and potential remote code execution; the Microsoft .NET advisory describes disclosure, denial of service and privilege escalation. These are possible impacts, not evidence that every affected service is remotely exploitable. The newly published October 25 Microsoft emergency updates and October 26 cross-ecosystem reporting make this a broad MSSP patch-priority issue. There is no verified malicious exploitation in the wild as of this report. Public proof-of-concept research has been reported, but no campaign-specific IPs, URIs or payload strings have been confirmed.

References

Impacted Systems

  • Component: Google Brotli C decoder 1.0.3–1.0.9, 1.1.0, 1.2.0; fixed in the maintained branches 1.0.10, 1.1.1, 1.2.1.
  • Node.js: 22.x before 22.23.4, 24.x before 24.22.1, 26.x before 26.12.1; deployed on Windows, Linux and macOS, including containers and self-hosted API/web services.
  • Microsoft .NET: 8.x before 8.0.33, 9.x before 9.0.22, 10.x before 10.0.14; fixed in those runtime releases. Self-contained and NativeAOT apps require rebuild/redeployment. .NET Framework is explicitly unaffected by this .NET advisory. ASP.NET Core and HttpClient do not enable Brotli decompression by default.
  • Python: Brotli before 1.2.1 and brotlicffi before 1.2.1.0, where used to decompress untrusted data.
  • Bun: 1.0.21 through 1.4.3; fixed in 1.4.4. Its fetch() can decode Brotli HTTP responses automatically.
  • Envoy: affected vendor releases 1.36.11, 1.37.7, 1.38.5 and 1.39.2 where Brotli decompression is configured; fixes 1.36.12, 1.37.8, 1.38.6, 1.39.3, 1.40.0.
  • Exposure: an untrusted compressed stream must reach a vulnerable decoder. Internet-facing service presence alone is insufficient; inbound HTTP headers may not be retained in Sentinel, and outbound compressed responses can also be an entry point. Other bundled decoder consumers require vendor-by-vendor validation.

Why this matters

Cross-platform libraries can be embedded in Internet-facing APIs, microservices, proxies, build workers and HTTP clients. Patching the host OS may not replace vendored Brotli inside runtime images or NativeAOT binaries. Behavioral hunts seek secondary evidence of process crashes or execution, not a CVE-specific signature.

Exploitation Status

Confirmed vulnerability and published fixes. Node.js/Google/Microsoft vendor advisories are authoritative for affected components. Public research reports proof-of-concept demonstrations, but no verified malicious exploitation in the wild as of 2026-10-27. Do not label generic HTTP 500s or shell child processes as confirmed Node2Shell exploitation.

What this hunt looks for

Queries target web-runtime child processes, Linux crashes, WAF/access-log 5xx spikes, unusual runtime egress, new executable/script writes and Windows 4688 process-creation fallbacks. No public Brotli exploit URI, header sequence, or IP is asserted; the first-pass hunt is post-exploitation behavior only.

Required logs

Each query requires its named Microsoft Sentinel table. No customer log ingestion or product deployment is assumed.

  • Hunt 01: DeviceProcessEvents
  • Hunt 02: Syslog
  • Hunt 03: AzureDiagnostics
  • Hunt 04: DeviceNetworkEvents
  • Hunt 05: DeviceFileEvents
  • Hunt 06: SecurityEvent

Hunt 01 — Unexpected shell spawned by a Brotli-capable application runtime

DeviceProcessEvents
| where TimeGenerated > ago(14d)
| where InitiatingProcessFileName in~ ("node.exe","node","dotnet.exe","dotnet","python.exe","python3","python")
| where FileName in~ ("cmd.exe","powershell.exe","pwsh.exe","bash","sh","dash","curl","wget","certutil.exe")
| where ProcessCommandLine has_any ("-enc","-c ","/c ","http://","https://","/tmp/","/dev/shm/","base64")
| project TimeGenerated, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine, FileName, ProcessCommandLine, AccountName
| order by TimeGenerated desc

Hunt 02 — Linux runtime segfault, core dump or Brotli decoder errors

Syslog
| where TimeGenerated > ago(14d)
| where SyslogMessage has_any ("segfault","core dumped","SIGSEGV","BrotliDecoder","brotli decoder")
| where SyslogMessage has_any ("node","dotnet","python","brotli","libbrotli") or ProcessName in~ ("node","dotnet","python","python3")
| project TimeGenerated, Computer, ProcessName, Facility, SeverityLevel, SyslogMessage
| order by TimeGenerated desc

Hunt 03 — Application Gateway access-log server-error concentration

AzureDiagnostics
| where TimeGenerated > ago(7d)
| where Category == "ApplicationGatewayAccessLog"
| extend Status = toint(column_ifexists("httpStatus_d", real(null))), Uri = tostring(column_ifexists("requestUri_s", "")), Client = tostring(column_ifexists("clientIP_s", ""))
| where Status between (500 .. 599)
| summarize ErrorCount=count(), ClientCount=dcount(Client), SampleUris=make_set(Uri, 5) by bin(TimeGenerated, 15m), ResourceId
| where ErrorCount >= 15
| order by ErrorCount desc

Hunt 04 — New public destinations contacted by Node or .NET processes

let baseline = DeviceNetworkEvents
| where TimeGenerated between (ago(30d) .. ago(2d))
| where InitiatingProcessFileName in~ ("node.exe","node","dotnet.exe","dotnet")
| summarize by DeviceName, RemoteUrl, RemoteIP;
DeviceNetworkEvents
| where TimeGenerated > ago(2d)
| where InitiatingProcessFileName in~ ("node.exe","node","dotnet.exe","dotnet")
| where isnotempty(RemoteIP) and RemoteIP !startswith "10." and RemoteIP !startswith "192.168." and RemoteIP !startswith "172.16."
| join kind=leftanti baseline on DeviceName, RemoteUrl, RemoteIP
| summarize Connections=count(), Processes=make_set(InitiatingProcessFileName, 5) by DeviceName, RemoteUrl, RemoteIP
| where Connections >= 2
| order by Connections desc

Hunt 05 — Runtime-created scripts and binaries in temporary locations

DeviceFileEvents
| where TimeGenerated > ago(14d)
| where InitiatingProcessFileName in~ ("node.exe","node","dotnet.exe","dotnet","python.exe","python3","python")
| where ActionType in~ ("FileCreated","FileModified","FileRenamed")
| where FolderPath has_any ("\\Temp\\","/tmp/","/dev/shm/","/var/tmp/")
| where FileName endswith ".sh" or FileName endswith ".ps1" or FileName endswith ".exe" or FileName endswith ".py" or FileName endswith ".dll"
| project TimeGenerated, DeviceName, InitiatingProcessFileName, ActionType, FileName, FolderPath, SHA256
| order by TimeGenerated desc

Hunt 06 — Windows 4688 fallback: runtime launching command interpreters

SecurityEvent
| where TimeGenerated > ago(14d) and EventID == 4688
| where ParentProcessName has_any ("node.exe","dotnet.exe","python.exe")
| where NewProcessName has_any ("cmd.exe","powershell.exe","pwsh.exe","certutil.exe")
| project TimeGenerated, Computer, Account, ParentProcessName, NewProcessName, CommandLine
| order by TimeGenerated desc

Detection Notes

Highest signal: unexpected shell/download-tool child processes of node, dotnet, or a service-hosted Python runtime, especially when accompanied by a crash or fresh egress. These behaviors are not specific to the CVE. Expected noise: deployment scripts, npm builds, health-check restarts, web application errors and routine runtime outbound calls. WAF/access logs typically omit compressed request/response bodies and the Content-Encoding: br header; therefore they cannot confirm the malformed Brotli stream. HTTP 500 spikes are triage pivots only. Device* tables require Defender endpoint telemetry ingested in Sentinel; SecurityEvent 4688 requires process creation (and command line if used); Syslog requires Linux host/application forwarding. No invented exploit IOCs.

Leave a Reply