On October 25, 2026, Microsoft shipped an out-of-band update fixing a Critical (CVSS 8.1) out-of-bounds write in the Brotli decoder used by .NET. It may let an attacker read or corrupt process memory, leading to information disclosure, DoS, or elevation of privilege.
Table of Contents
What is Brotli?
Brotli is Google’s compression algorithm, widely used on the web via Content-Encoding: br. It has two sides: the encoder (compresses) and the decoder (decompresses). The bug is in the decoder, the side that processes incoming data, which is exactly where an attacker can feed crafted input.
How is .NET involved?
.NET doesn’t implement Brotli itself. It bundles Google’s native Brotli library in the runtime and wraps it in managed APIs. When the upstream decoder has a bug, .NET apps using it inherit it.
Patched versions
| .NET | Runtime | SDK |
|---|---|---|
| 8 | 8.0.33 | 8.0.133 / 8.0.427 |
| 9 | 9.0.22 | 9.0.123 / 9.0.320 |
| 10 | 10.0.14 | 10.0.114 / 10.0.403 |
| 11 | RC3 | Upgrade from RC2 |
What code is at risk?
You’re exposed if your app decompresses Brotli data from untrusted sources:
- HttpClient automatic decompression
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.All // includes Brotli
};
- ASP.NET Core request decompression
builder.Services.AddRequestDecompression(); app.UseRequestDecompression();
- Direct Brotli APIs Call
using var brotli = new BrotliStream(input, CompressionMode.Decompress); // or BrotliDecoder.TryDecompress(...)
Risky when input comes from uploads, third parties, or queues you don’t control.
Not affected: apps using none of the above (options 1 and 2 are off by default), compression-only code like response compression, and .NET Framework.
Find usages
- Via grep
grep -rnE "Brotli|AutomaticDecompression|UseRequestDecompression|AddRequestDecompression" --include=*.cs .
- Via powershell
Get-ChildItem -Recurse -Filter *.cs -File |
Where-Object { $_.FullName -notmatch '\\(bin|obj)\\' } |
Select-String -Pattern "Brotli|AutomaticDecompression|UseRequestDecompression|AddRequestDecompression" |
Select-Object Path, LineNumber, Line
- dotnet command for list third-party NuGet packages that ship their own native Brotli, since a runtime update won’t patch those.
dotnet list package --include-transitive | Select-String -Pattern "brotli"
Mitigation
📌Can’t patch yet? Restrict to DecompressionMethods.GZip | DecompressionMethods.Deflate and disable request decompression on public endpoints.
📌Update Runtime / SDK from Patched versions Table
📌Lock versions, in global.json sets the minimum SDK; builds fail on anything older
{
"sdk": {
"version": "10.0.403",
"rollForward": "latestPatch",
"allowPrerelease": false
}
}
📌Dockerfile pins both stages. The CVE is in the runtime, so updating only the SDK image isn’t enough:
FROM mcr.microsoft.com/dotnet/sdk:10.0.403 AS build WORKDIR /src COPY global.json *.sln ./ COPY src/MyApp/*.csproj src/MyApp/ RUN dotnet restore COPY . . RUN dotnet publish src/MyApp -c Release -o /app/publish --no-restore FROM mcr.microsoft.com/dotnet/aspnet:10.0.14 AS final WORKDIR /app COPY --from=build /app/publish . USER $APP_UID ENTRYPOINT ["dotnet", "MyApp.dll"]
Verify the tags exist on MCR, and build with --pull to avoid stale cached layers.
📌On CI/CD
- GitHub Actions uses global.json as the single source of truth and fails if the runtime is too old:
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Verify patched runtime
run: |
MIN="10.0.14"
CUR=$(dotnet --list-runtimes | grep "Microsoft.NETCore.App 10\." | awk '{print $2}' | sort -V | tail -1)
[ "$(printf '%s\n' "$MIN" "$CUR" | sort -V | head -1)" = "$MIN" ] || { echo "Runtime $CUR < $MIN"; exit 1; }
- run: docker build --pull -t myapp:${{ github.sha }} .
- GitLab CI pins the SDK image and fails fast if the runtime is too old:
verify-runtime:
stage: verify
image: mcr.microsoft.com/dotnet/sdk:10.0.403
variables:
MIN_RUNTIME: "10.0.14"
script:
- |
CUR=$(dotnet --list-runtimes | grep "Microsoft.NETCore.App 10\." | awk '{print $2}' | sort -V | tail -1)
[ "$(printf '%s\n' "$MIN_RUNTIME" "$CUR" | sort -V | head -1)" = "$MIN_RUNTIME" ] || { echo "Runtime $CUR < $MIN_RUNTIME"; exit 1; }
build-image:
stage: image
image: docker:27
services: [docker:27-dind]
script:
- docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
- Jenkins (Declarative Pipeline) runs the build inside the pinned SDK container:
pipeline {
agent none
environment { MIN_RUNTIME = '10.0.14' }
stages {
stage('Verify + Build') {
agent { docker { image 'mcr.microsoft.com/dotnet/sdk:10.0.403' } }
steps {
sh '''
CUR=$(dotnet --list-runtimes | grep "Microsoft.NETCore.App 10\\." | awk '{print $2}' | sort -V | tail -1)
[ "$(printf '%s\\n' "$MIN_RUNTIME" "$CUR" | sort -V | head -1)" = "$MIN_RUNTIME" ] || { echo "Runtime too old"; exit 1; }
dotnet build -c Release
dotnet test -c Release --no-build
'''
}
}
stage('Build image') {
agent any
steps { sh 'docker build --pull -t myapp:${BUILD_NUMBER} .' }
}
}
}
Recap-CheckList
- Container images: rebuild and redeploy. Editing the Dockerfile alone fixes nothing in production.
- Self-contained apps: republish, since the runtime is bundled.
- Self-hosted build agents: update the SDK installed on each machine.
- Patched on Oct 13? Update again. That release didn’t fully fix CVE-2026-70197 and CVE-2026-102870, and Blazor WebAssembly users must also bump NuGet references.
- Keep pins current: use Dependabot or Renovate to raise PRs for base images and
global.json.



