Microsoft fixed 46 vulnerabilities in .NET on 13 October 2026, and one of them won't be fully fixed by the update. CVE-2026-70175 lets ASP.NET Core Identity send a password-reset link to the address an attacker typed instead of the address on the account. Microsoft patched the default Identity UI. But if your app started from the Blazor Web App template with individual accounts, the vulnerable code lives in your own repository, and no runtime update will touch it.
This post is for teams running .NET 8, 9 or 10 in production. It ranks the release by what to patch first, explains the Identity bug and the small fix for your own code, and gives you a script to check every runtime and SDK on a machine.
The short answer
Update to .NET 8.0.32, 9.0.21 or 10.0.13 (SDKs 8.0.426, 9.0.319 or 10.0.402), released 13 October 2026. Then restart your apps and rebuild anything self-contained. As of 14 October, Microsoft's Security Update Guide marks none of the 46 CVEs as exploited. One, CVE-2026-92707, is marked publicly disclosed. Microsoft rates two as Critical: a heap overflow in Microsoft QUIC on Windows (CVE-2026-105016, CVSS 9.8) and a signed-XML flaw that MSRC titles "AD FS Spoofing" (CVE-2026-96222). The runtime update doesn't cover that second one on modern .NET: it's fixed in the System.Security.Cryptography.Xml NuGet package, where 8.0.0 through 8.0.4 are affected and 8.0.5 is fixed, so check your package references as well. Not every CVE applies to every version either. Microsoft's release metadata lists 40 CVEs for 8.0.32, 42 for 9.0.21 and 44 for 10.0.13; the others aren't part of that runtime release, for example CVE-2026-96222 (fixed in a NuGet package) and CVE-2026-70194 (.NET 11 RC2 only). If you use ASP.NET Core Identity, also change the password-reset and resend-confirmation pages in your own code to email the stored address (details below). .NET 8 and .NET 9 both reach end of support on 10 November 2026, four weeks from now.
What to patch first
The release covers 14 denial-of-service bugs, 13 remote code execution, 7 elevation of privilege, 6 security feature bypasses, 5 information disclosures and 1 spoofing issue. Most teams need only a handful of rows from that list, so here are the ones that change your order of work.
| CVE | CVSS | What it is | Who should move first |
|---|---|---|---|
| CVE-2026-105016 | 9.8 | Heap overflow in Microsoft QUIC: a crafted IPv6 QUIC packet reaches the HTTP/3 path, no authentication | Windows hosts running .NET apps that accept QUIC (HTTP/3) |
| CVE-2026-88092 | 9.4 | Kestrel HTTP/1: an aborted connection can reparse unread body bytes as a new request (request smuggling) | Kestrel behind a reverse proxy or load balancer |
| CVE-2026-70188 | 9.1 | Kestrel HTTP/2 accepts newlines in trailers and HPACK dynamic-table values | Kestrel serving HTTP/2, particularly behind a proxy that translates HTTP/2 to HTTP/1 |
| CVE-2026-70175 | 8.1 | Identity reset and confirmation mail sent to the request address, not the stored one | Every app using ASP.NET Core Identity with email reset |
| CVE-2026-96237 | 7.5 | SDK workload downloads and repairs can bypass NuGet Package Source Mapping | CI agents and build machines |
| CVE-2026-96957 | 8.8 | Signature verification for .NET file-based apps can skip the expected trust decision | Anyone running dotnet run app.cs on SDK 10 or 11 |
Request smuggling is worth a moment, because two of the three highest scores are Kestrel parsing bugs. Smuggling only pays off when two HTTP parsers disagree about where a request ends: usually your proxy and your app server. If Kestrel faces the internet directly, the risk is lower. If it sits behind nginx, an ingress controller or a cloud load balancer, which describes most production deployments, treat these two as urgent. For background on why parser disagreements are so dangerous, see our HTTP request smuggling explainer.
Eleven of the 46 advisories are in WPF and XPS document handling (Microsoft.WindowsDesktop.App). Eight of them carry an 8.8 "remote code execution" score, and all eight need a user to open crafted content (UI:R in the CVSS vector). They matter for desktop apps that render documents from outside, not for web servers. Two more are in Blazor WebAssembly debugging (CVE-2026-96230 and CVE-2026-102870) and affect developer machines running dotnet watch or the debug proxy, not production.
The Identity bug: the lookup and the send used different addresses
Microsoft's advisory is short: Identity's "password-reset and confirmation flows can send mail to the unvalidated request address instead of the user's stored canonical address." Microsoft built the fix internally, so it doesn't appear in the public dotnet/aspnetcore tags. It does appear in the dotnet/dotnet source repository, between tags v10.0.112 and v10.0.113. Here is the forgot-password handler from the default Identity UI, before and after:
// Before (v10.0.112)
var user = await _userManager.FindByEmailAsync(Input.Email);
// ... generate the reset token and callback URL ...
await _emailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
// After (v10.0.113)
var storedEmail = await _userManager.GetEmailAsync(user);
if (string.IsNullOrEmpty(storedEmail))
{
throw new InvalidOperationException("Users must have an email.");
}
await _emailSender.SendPasswordResetLinkAsync(user, storedEmail, HtmlEncoder.Default.Encode(callbackUrl));
The resend-confirmation page received the same change.
The problem is that FindByEmailAsync doesn't compare the raw string. It runs both sides through a lookup normalizer, and Identity's default, UpperInvariantLookupNormalizer, does email.Normalize().ToUpperInvariant(). So several different strings find the same account, and the old code then emailed the string the requester typed, along with a valid reset token for that account. You can see how wide the match is with a few lines of C#:
// normtest: dotnet run
// Identity's default UpperInvariantLookupNormalizer, reproduced.
static string Normalize(string email) => email.Normalize().ToUpperInvariant();
string stored = "sysadmin@kontoso.example";
string[] submitted =
{
"SysAdmin@Kontoso.example",
"\u017Fysadmin@kontoso.example", // U+017F LATIN SMALL LETTER LONG S
"sysadmin@\u212Aontoso.example", // U+212A KELVIN SIGN
"sys.admin@kontoso.example",
};
foreach (var s in submitted)
Console.WriteLine($"{s,-26} finds the account: {Normalize(s) == Normalize(stored),-5} same string: {s == stored}");
Output on .NET 8.0.31:
SysAdmin@Kontoso.example finds the account: True same string: False
ſysadmin@kontoso.example finds the account: True same string: False
sysadmin@Kontoso.example finds the account: True same string: False
sys.admin@kontoso.example finds the account: False same string: False
Three different strings find the same account. The long s and the Kelvin sign are not exotic edge cases picked by hand: we ran every character in Unicode's Basic Multilingual Plane through the same normalizer on .NET 8, and those are the only two non-ASCII characters that collapse into plain ASCII letters. One thing we checked and dropped: the Turkish dotless ı, the character behind several well-known reset bugs on other platforms, does not match under .NET's invariant upper-casing.
To be fair about the limits: Microsoft hasn't said how an attacker gets the misdirected email delivered to a mailbox they control, and rates exploitation "less likely." Most mail providers treat the local part of an address without regard to case, so a case variant usually lands in the victim's own inbox. The risk grows with anything that makes matching looser than delivery: a custom ILookupNormalizer that strips dots or plus tags, a mail relay that rewrites Unicode, or a user store that compares more loosely than you think. The fix removes the whole class, because the mail now goes only to the address the account already has.
Your template code doesn't get the fix
ASP.NET Core ships Identity's account pages in two ways, and they are patched differently.
- The default Identity UI is a Razor Class Library in the
Microsoft.AspNetCore.Identity.UINuGet package. Microsoft fixed it. Update the package reference to 8.0.32, 9.0.21 or 10.0.13 (all three are on NuGet) along with the runtime. - The Blazor Web App template (
dotnet new blazor --auth Individual) copies its account pages into your project as.razorfiles when you create the app. They're your code from that moment. In thev10.0.113source tag, the template'sForgotPassword.razorandResendEmailConfirmation.razorstill send toInput.Email. So do Identity pages you scaffolded into an MVC or Razor Pages app before the fix.
Check your repositories:
grep -rnE "Send(PasswordResetLink|ConfirmationLink)Async\([^,]+,\s*Input\.Email" \
--include='*.razor' --include='*.cs' .
On a fresh Blazor app generated with SDK 8.0.425, that returns four hits (lines trimmed):
App/Components/Account/Pages/ResendEmailConfirmation.razor:57: ...SendConfirmationLinkAsync(user, Input.Email, ...
App/Components/Account/Pages/ForgotPassword.razor:57: ...SendPasswordResetLinkAsync(user, Input.Email, ...
App/Components/Account/Pages/Register.razor:92: ...SendConfirmationLinkAsync(user, Input.Email, ...
App/Components/Account/Pages/ExternalLogin.razor:151: ...SendConfirmationLinkAsync(user, Input.Email, ...
The first two are the ones to fix, because they look up an existing account by the submitted address. Register and ExternalLogin create a new account from Input.Email, so the submitted address is the stored address and those calls are fine. In ForgotPassword.razor, the change mirrors Microsoft's:
var storedEmail = await UserManager.GetEmailAsync(user);
if (string.IsNullOrEmpty(storedEmail))
{
throw new InvalidOperationException("Users must have an email.");
}
await EmailSender.SendPasswordResetLinkAsync(user, storedEmail, HtmlEncoder.Default.Encode(callbackUrl));
Apply the same change to ResendEmailConfirmation.razor with SendConfirmationLinkAsync. The rule generalises to any account flow you write yourself: look the user up with whatever normalization you like, then act on the values stored on the account, never on the values in the request.
Check every runtime and SDK on a machine
Updating the runtime on a server doesn't help a self-contained app that bundled the old one, and build agents often carry several SDKs side by side. This script compares what dotnet reports against the first fixed version on each line:
#!/usr/bin/env python3
"""Flag .NET runtimes and SDKs on this machine that predate the 13 October 2026 security release."""
import re, subprocess, sys
DOTNET = sys.argv[1] if len(sys.argv) > 1 else "dotnet"
# First fixed version per servicing line, from Microsoft's October 2026 advisories.
FIXED_RUNTIME = {"8.0": (8, 0, 32), "9.0": (9, 0, 21), "10.0": (10, 0, 13)}
FIXED_SDK = {"8.0.1": 132, "8.0.4": 426, "9.0.1": 122, "9.0.3": 319, "10.0.1": 113, "10.0.4": 402}
EOL = {"8.0": "2026-11-10", "9.0": "2026-11-10"}
def run(arg):
return subprocess.run([DOTNET, arg], capture_output=True, text=True, check=True).stdout
def ver(s):
return tuple(int(x) for x in re.match(r"(\d+)\.(\d+)\.(\d+)", s).groups())
bad = 0
for line in run("--list-runtimes").splitlines():
name, v = line.split()[:2]
line_key = ".".join(v.split(".")[:2])
fixed = FIXED_RUNTIME.get(line_key)
if "-" in v or fixed is None:
status = "preview or out of support: check manually"
bad += 1
elif ver(v) < fixed:
status = f"VULNERABLE: update to {'.'.join(map(str, fixed))}"
bad += 1
else:
status = "ok"
eol = f" (support ends {EOL[line_key]})" if line_key in EOL else ""
print(f"runtime {name:<34} {v:<12} {status}{eol}")
for line in run("--list-sdks").splitlines():
v = line.split()[0]
major, minor, patch = ver(v)
band = f"{major}.{minor}.{str(patch)[0]}"
fixed = FIXED_SDK.get(band)
if "-" in v or fixed is None:
status = "preview or unknown feature band: check manually"
bad += 1
elif patch < fixed:
status = f"VULNERABLE: update to {major}.{minor}.{fixed}"
bad += 1
else:
status = "ok"
print(f"sdk {v:<46} {status}")
sys.exit(1 if bad else 0)
On a machine that hadn't been updated yet:
runtime Microsoft.AspNetCore.App 8.0.31 VULNERABLE: update to 8.0.32 (support ends 2026-11-10)
runtime Microsoft.NETCore.App 8.0.31 VULNERABLE: update to 8.0.32 (support ends 2026-11-10)
sdk 8.0.425 VULNERABLE: update to 8.0.426
It exits non-zero when anything needs attention, so it can gate a build image. For NuGet package references (Identity UI, System.Security.Cryptography.Xml, System.Security.Cryptography.Pkcs, the SignalR packages), dotnet list package --vulnerable covers the rest; we walk through it in finding vulnerable .NET dependencies.
The date that matters more than the CVEs
Microsoft's release metadata lists 10 November 2026 as the end of support for both .NET 8 (LTS) and .NET 9 (STS). After that date, the next Kestrel parsing bug or Identity flaw on those lines won't get a patch. If you are still on 8 or 9, this month's update is worth doing quickly, but the work that matters is the move to .NET 10, which is supported until November 2028.
If you want runtime and package versions like these tracked across every repository instead of one machine at a time, that's what Safeguard SCA does.