# Brotli2Shell hits Node.js and .NET: your Azure patch plan

CVE-2026-111497 is an out-of-bounds write in Google's Brotli decoder that Node.js says permits remote code execution, and it reaches Node.js 22, 24 and 26, .NET 8, 9 and 10, and the PyPI Brotli package. Fixes landed on 21, 22 and 25 October 2026, Imperva has a working exploit on Node 24.22.0, Node 20 will never be patched, and this guide covers who patches what in Azure, which code paths decode untrusted Brotli, and a seven-step audit.

- Published: 2026-10-26 · Category: Security & Compliance · Tags: Brotli2Shell, Node2Shell, CVE-2026-111497, Node.js, .NET, Azure App Service, Azure Functions, Azure Container Apps, Azure Front Door, Next.js, NIS2, Cyber Resilience Act
- Author: Technspire AB, Stockholm (https://technspire.com)
- Canonical: https://technspire.com/en/blog/brotli2shell-hits-nodejs-and-dotnet-your-azure-patch-plan

On 21 October 2026 the Node.js project shipped 22.23.4, 24.22.1 and 26.12.1 to close ten CVEs. One of them, CVE-2026-111497, is an out-of-bounds write in Google's Brotli decoder that the advisory says "permits remote code execution", and it was reported by OpenAI's Codex Security team. Because the bug lives in the Brotli C library rather than in Node, it did not stay a Node problem. Google published Brotli 1.0.10, 1.1.1 and 1.2.1 the same evening. The PyPI Brotli package followed on 22 October. Microsoft shipped out-of-band security updates for .NET 8, 9 and 10 on Saturday 25 October. Imperva's red team had code execution against the official Node.js 24.22.0 image within a day of disclosure and named it Node2Shell; JFrog's follow-up, which traced the same decoder into FFmpeg, ClickHouse and Apache Arrow, calls it Brotli2Shell. For an Azure estate this means three runtimes to patch in one week, four days after PHP2Shell.

## What the bug is

The Node.js advisory is precise about the mechanism. An invalid block switch in a Brotli stream is reported by the decoder as a temporary need for more input instead of a permanent format error, and the malformed input can then write past an internal 8-byte decoder buffer. The result ranges from a crash, through disclosure of process memory, to execution of attacker-supplied code. The affected library range is Brotli 1.0.3 through 1.2.0, which covers every release since 2018. The fix adds a new error constant, BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, so a patched decoder now rejects the stream outright.

Three organisations scored the same bug three ways, and the gap tells you something about the attack preconditions. Node.js rates it High. The GitHub advisory for the PyPI packages gives it 9.2 Critical on CVSS 4.0. Microsoft's .NET advisory gives it 8.1 High on CVSS 3.1 with attack complexity set to High. The precondition every scorer agrees on: your process has to feed attacker-controlled Brotli data into the decoder. Nothing happens to a service that only compresses its own responses. The question for your audit is therefore not "do I run Node or .NET" but "where does my code decompress something it did not produce".

## How exploitable it is this week

Imperva demonstrated two working routes on the official Node.js 24.22.0 Linux arm64 image. The first is Node's built-in fetch() consuming a Brotli-compressed HTTP response, which it decompresses automatically. The second is Express 5.0.0 through 5.3.0, or body-parser 2.0.0 through 2.3.0, decoding a Brotli-compressed request body. Node.js 24.22.1 rejected the same inputs. Imperva notes the exploit "required several tries", and Sysdig's detection write-up adds that another team found blind remote execution did not work with address randomisation on. Crashes are the reproducible outcome everywhere; reliable code execution has so far been shown in narrow configurations. No exploitation in the wild had been reported as of 22 October.

Read that as two separate risks with two separate timelines. Denial of service is certain today for any exposed decoder, and a 23 MB crafted body was enough to take down ClickHouse in JFrog's testing. Code execution is plausible, improving with each public write-up, and the x64 case is where researchers are now spending their time. Patch on the RCE timeline, not the DoS one.

| Component | Vulnerable | Fixed | Fix date |
| --- | --- | --- | --- |
| Google Brotli (C library) | 1.0.3 to 1.2.0 | 1.0.10, 1.1.1, 1.2.1 | 21 Oct |
| Node.js 22 LTS | below 22.23.4 | 22.23.4 | 21 Oct |
| Node.js 24 LTS | below 24.22.1 | 24.22.1 | 21 Oct |
| Node.js 26 Current | below 26.12.1 | 26.12.1 | 21 Oct |
| Node.js 20, 23, 25 and older | all versions | none, end-of-life | never |
| .NET 8 | below 8.0.33 | 8.0.33 (SDK 8.0.133 / 8.0.427) | 25 Oct |
| .NET 9 | below 9.0.22 | 9.0.22 (SDK 9.0.123 / 9.0.320) | 25 Oct |
| .NET 10 | below 10.0.14 | 10.0.14 (SDK 10.0.114 / 10.0.403) | 25 Oct |
| .NET 11 previews | before RC3 | 11.0.0-rc.3 | 25 Oct |
| .NET Framework | not affected | n/a | n/a |
| PyPI Brotli | 1.0.3 to 1.2.0 | 1.2.1 | 22 Oct |
| PyPI brotlicffi | 1.0.3 to 1.2.0.x | 1.2.1.0 | 22 Oct |

## Where the decoder actually runs in your code

Patching the runtime is the complete fix. Knowing which services sit on the exposed paths tells you which to patch first, and which ones deserve a code-level guard while the rollout is in flight.

- **Node.js, outbound.** Every server-side fetch() in Node 18 and later transparently decompresses a br response. That covers Next.js server components and route handlers calling third-party APIs, webhook verifiers that call back to a URL the sender supplied, link-preview and RSS fetchers, OpenID discovery against a tenant-configured issuer, and MCP clients talking to servers your users registered. Anywhere an attacker can choose the host your server talks to, the attacker chooses the response.
- **Node.js, inbound.** Express 5 and body-parser 2 decode br request bodies when inflate is on, which is the default. Any public JSON API on those versions is a target. Next.js does not decompress request bodies on its own, so a pure Next.js app without a custom Express server is on the outbound path only. Direct calls to zlib.brotliDecompress on uploaded files, cache entries or message payloads are the third route.
- **.NET.** The affected APIs are BrotliStream, BrotliDecoder, ASP.NET Core request decompression and HttpClient automatic response decompression. Microsoft stresses that the last two are not enabled by default. Once you add the request decompression middleware, br is in its default provider list on every version from .NET 7 up, and an HttpClient handler configured with DecompressionMethods.All accepts br. The response compression middleware is not affected, because it only encodes.
- **Python.** The Brotli and brotlicffi packages are optional extras for urllib3, requests, httpx and aiohttp. If either package is installed, those clients advertise br and decode it. pyarrow decodes Brotli-compressed Parquet columns, and JFrog triggered an infinite loop there with a 534-byte input.
- **Everything with a native decoder.** FFmpeg via libjxl for JPEG XL images, ClickHouse for HTTP request bodies, and whatever your vendors have statically linked. A Functions app that shells out to ffmpeg for thumbnails is on this list.

## Who patches what in Azure

The same split that decided PHP2Shell applies here. Microsoft owns the runtime in some places and you own it in others, and the portal does not always make the boundary obvious.

- **App Service, built-in Node and .NET runtimes.** App Service's published policy is that an app "will be automatically updated to the latest patch version available in the platform for the configured runtime", and that pinning a minor or patch version is not recommended. That is the mechanism that gets you to 24.22.1 or 8.0.33 without a deploy. Verify it rather than assume it: open SSH or Kudu on one app per plan and read node -v or dotnet --list-runtimes. An app still on NODE|20-lts has had no security fixes since 30 April 2026 and will not get this one.
- **App Service, self-contained .NET.** A self-contained publish carries its own copy of System.IO.Compression. The platform update does nothing for it. Rebuild with the 8.0.133, 9.0.123 or 10.0.114 SDK and redeploy.
- **Azure Functions.** Node.js 22 and 24 are the only GA Node versions, and Functions upgrades minor versions automatically for the same reason App Service does. .NET isolated worker apps on 8, 9 and 10 are framework-dependent by default and ride the platform runtime. The in-process .NET 8 model reaches end of support on 10 November 2026, so a function app you are touching this week for Brotli is one you should migrate at the same time.
- **Container Apps, AKS and custom containers on App Service.** Yours entirely. The decoder is baked into the image until you rebuild from node:24.22.1, node:22.23.4, or the 8.0.33, 9.0.22 and 10.0.14 tags of mcr.microsoft.com/dotnet/aspnet. Pin by digest afterwards.
- **Virtual machines.** Node installed through nvm or a tarball is yours; Node and libbrotli from the distribution come through the distribution's security channel. Both need checking.
- **Managed analytics.** Fabric, Synapse and Databricks notebooks ship their own pyarrow and Brotli builds. Ask the vendor for a dated statement rather than guessing.

## The audit

Inventory first, verification second, code-level guards third. The WAF step comes last because it only covers one of the attack routes.

### 1. Inventory every Node, .NET and Python site

```
# App Service and Functions across every subscription you can read
az graph query -q "
resources
| where type =~ 'microsoft.web/sites'
| extend linuxFx = tostring(properties.siteConfig.linuxFxVersion),
         winNet  = tostring(properties.siteConfig.netFrameworkVersion),
         winNode = tostring(properties.siteConfig.nodeVersion)
| where linuxFx startswith 'NODE' or linuxFx startswith 'DOTNET'
     or linuxFx startswith 'PYTHON' or linuxFx startswith 'DOCKER'
     or isnotempty(winNode) or winNet startswith 'v8' or winNet startswith 'v9' or winNet startswith 'v10'
| project name, resourceGroup, location, kind, linuxFx, winNet, winNode
" -o table

# Container Apps: which image is live in each revision
az graph query -q "
resources
| where type =~ 'microsoft.app/containerapps'
| mv-expand c = properties.template.containers
| project name, resourceGroup, image = tostring(c.image)
" -o table
```

Anything reporting NODE|20-lts or older goes to the top of the list, because no patch exists for it. Anything under DOCKER is a rebuild, not a wait.

### 2. Verify the running version, not the configured one

```
# One app per App Service plan
az webapp ssh -n <app> -g <rg>
node -v                       # want 22.23.4+, 24.22.1+ or 26.12.1+
dotnet --list-runtimes        # want 8.0.33+, 9.0.22+ or 10.0.14+
pip show brotli brotlicffi    # want 1.2.1 / 1.2.1.0, or not installed

# Container image, locally
docker run --rm --entrypoint node <image> -v
docker run --rm --entrypoint dotnet <image> --list-runtimes
```

### 3. Find the decoder in your repositories

```
# Node: direct decoder use and request-body inflation
grep -rnE "brotliDecompress|createBrotliDecompress|body-parser|express\.json\(|express\.urlencoded\(" --include=*.js --include=*.ts .

# Node: every server-side fetch is an outbound decoder
grep -rnE "\bfetch\(" --include=*.ts --include=*.tsx app lib

# .NET
grep -rnE "BrotliStream|BrotliDecoder|AddRequestDecompression|DecompressionMethods\.(All|Brotli)" --include=*.cs .

# Python
grep -rniE "^(brotli|brotlicffi|pyarrow)" requirements*.txt pyproject.toml poetry.lock 2>/dev/null
```

### 4. Rebuild every container on a fixed base

```
# Dockerfile, Node
FROM node:24.22.1-bookworm-slim

# Dockerfile, .NET
FROM mcr.microsoft.com/dotnet/sdk:8.0.133 AS build
FROM mcr.microsoft.com/dotnet/aspnet:8.0.33

# Force a fresh pull of the base, then pin the result by digest
docker build --pull -t <acr>.azurecr.io/<app>:2026-10-26 .
az acr repository show-manifests -n <acr> --repository <app> --query "[0].digest"
```

### 5. Code-level guards while the rollout runs

These are temporary. They remove the decoder from the request path without touching the runtime, and they are what you reach for on a service you cannot redeploy today.

```
// Node: stop advertising br on outbound calls you do not control
await fetch(url, { headers: { 'accept-encoding': 'gzip, deflate' } });

// Express 5 / body-parser 2: refuse compressed request bodies entirely
app.use(express.json({ inflate: false }));

// ASP.NET Core: keep request decompression, drop Brotli
builder.Services.AddRequestDecompression(o => o.DecompressionProviders.Remove("br"));

// .NET HttpClient: never use DecompressionMethods.All on untrusted hosts
new SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate };

# Python: remove the optional extra until 1.2.1 is pinned
pip uninstall brotli brotlicffi
```

### 6. A Front Door rule for the inbound route only

Azure Front Door WAF custom rules match on RequestHeader with a header-name selector, so a rule that blocks any request declaring Content-Encoding: br is a few lines of JSON. Legitimate clients almost never send Brotli-compressed request bodies, so the false-positive cost is low. Start in Log action, watch for a day, then switch to Block. Be clear with yourself about what it covers: inbound bodies to Express or ASP.NET Core. It does nothing for the fetch() route, where your server is the client and the attacker's server chooses the encoding.

```
{
  "name": "BlockBrotliRequestBodies",
  "priority": 10,
  "ruleType": "MatchRule",
  "matchConditions": [{
    "matchVariable": "RequestHeader",
    "selector": "Content-Encoding",
    "operator": "Contains",
    "transforms": ["Lowercase"],
    "matchValue": ["br"]
  }],
  "action": "Block"
}
```

### 7. Detect the attempts you cannot block

A failed exploit is a crashed process. Alert on Node and .NET worker restarts in App Service and on container restart counts in Container Apps and AKS, and correlate a crash with any process spawned from the same workload in the following minutes, which is the pattern Sysdig's managed rule looks for. On patched runtimes, a flood of BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH errors in your logs is someone probing you, and worth a look at the source addresses.

## The Swedish and EU angle

Two things make this one different from a routine patch Tuesday for an organisation under NIS2 or its Swedish transposition. The first is that the decoder is inside products you buy, not only code you write. The Cyber Resilience Act's 24-hour reporting duty, in force since 11 September 2026, triggers on actively exploited vulnerabilities, and nobody has reported active exploitation of CVE-2026-111497. So your vendors are under no legal clock to tell you whether their appliance, agent or SaaS connector embeds a vulnerable Brotli. Ask them anyway, in writing, and treat a product whose vendor cannot answer within the week as unpatched.

The second is the discoverer. The Node.js advisory credits OpenAI Codex Security for this bug, and the broader pattern of October 2026 is model-assisted vulnerability hunting producing critical memory-safety findings in foundational libraries at a cadence the ecosystem has not seen before. Plan for the next one. An estate that can rebuild every container image and verify every App Service runtime inside a working day is the realistic defence; an estate that needs a change-advisory-board meeting to bump a base image is going to be doing this on a weekend, as the .NET team did on 25 October.

One date to fold into the same change window: Next.js 15 left Maintenance LTS on 21 October 2026, two years after its release, so Next.js 16 is now the only supported line. If you are redeploying a Next.js app on a patched Node anyway, do the framework upgrade in the same pull request rather than carrying an unsupported framework on a patched runtime.

## Checklist

- Run the Resource Graph queries and keep the output. Node 20 apps and anything under DOCKER go first.
- SSH into one app per plan and confirm 22.23.4, 24.22.1 or 26.12.1 for Node and 8.0.33, 9.0.22 or 10.0.14 for .NET.
- Grep the repositories for decoder use and classify each hit as inbound, outbound or file-based.
- Rebuild every container on a patched base image today, pin by digest, and verify the running revision.
- Apply the code-level guards to anything that cannot redeploy this week, and remove them once the runtime is confirmed.
- Add the Front Door rule in Log mode, then Block, and record that it covers inbound bodies only.
- Alert on worker crashes and container restarts, and send every vendor with an embedded runtime a dated question.

## Sources

- [Node.js: October 2026 Security Releases (CVE-2026-111497 and nine others)](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases)
- [Node.js 24.22.1 (LTS) release notes](https://nodejs.org/en/blog/release/v24.22.1)
- [Google Brotli releases: 1.0.10, 1.1.1 and 1.2.1](https://github.com/google/brotli/releases)
- [GitHub Advisory GHSA-9v8h-qqgm-4m35: PyPI Brotli and brotlicffi, CVSS 4.0 9.2](https://github.com/advisories/GHSA-9v8h-qqgm-4m35)
- [Microsoft Security Advisory CVE-2026-111497: .NET Brotli decoder out-of-bounds write](https://github.com/dotnet/announcements/issues/494)
- [.NET Blog: October 2026 out-of-band security updates](https://devblogs.microsoft.com/dotnet/dotnet-october-2026-oob-security-updates/)
- [Imperva: Node2Shell, from Brotli data to remote code execution](https://www.imperva.com/blog/node2shell-from-brotli-data-to-remote-code-execution/)
- [JFrog Security Research: Brotli2Shell, beyond Node2Shell and HTTP2Shell](https://research.jfrog.com/post/brotli2shell-beyond-node2shell-and-http2shell)
- [Sysdig: Detecting the Node.js Brotli RCE](https://www.sysdig.com/blog/node2shell-cve-2026-111497-detecting-the-node-js-brotli-rce)
- [Microsoft Learn: Request decompression in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/request-decompression)
- [Azure App Service: Node.js runtime support and patch policy](https://github.com/Azure/app-service-linux-docs/blob/master/Runtime_Support/node_support.md)
- [Microsoft Learn: Supported languages in Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/supported-languages)
- [Microsoft Learn: Azure Front Door WAF custom rules](https://learn.microsoft.com/en-us/azure/web-application-firewall/afds/waf-front-door-custom-rules)
- [Next.js support policy: 15.x Maintenance LTS ends 21 October 2026](https://nextjs.org/support-policy)
- [Technspire: PHP2Shell on Azure, your App Service and WAF audit](https://technspire.com/en/blog/php2shell-on-azure-your-app-service-and-waf-audit)

---

Technspire AB builds AI agents, Azure OpenAI solutions, and production web platforms for Swedish and EU enterprises. Book a call: https://calendly.com/technspire · hello@technspire.com · More articles: https://technspire.com/en/blog · Site overview for agents: https://technspire.com/llms.txt
