Safeguard
Application Security•

.NET 8 and .NET 9 End of Support on 10 November 2026: What Stops, Why Scanners Go Quiet, and How to Find Every Pin

.NET 8 and .NET 9 both reach end of support on 10 November 2026. After that date Microsoft ships no fixes for either, security fixes included, and the official container images for both stop being rebuilt. Your vulnerability scanner will also go quiet about them, for a reason that has nothing to do with being safe.

Safeguard Security Team
Security Research
12 min read

.NET 8 and .NET 9 both reach end of support on 10 November 2026. After that date Microsoft ships no fixes for either, security fixes included, and the official container images for both stop being rebuilt. Your vulnerability scanner will also go quiet about them, for a reason that has nothing to do with being safe.

This guide is for teams running ASP.NET Core apps, worker services or Azure Functions on .NET 8 or 9. It covers what changes on the date, how much patching you lose, a script that finds every place a repository pins an affected version, a stopgap for hosts you can't redeploy in time, and the .NET 10 changes most likely to trip up a migration.

The short answer

.NET 8 (LTS) and .NET 9 (STS) are supported until 10 November 2026. Move to .NET 10, which is supported until 14 November 2028. The latest patches are 8.0.32 and 9.0.21, released on 13 October 2026; install them now regardless of your migration date. Azure Functions apps on the in-process model hit a separate deadline on the same day and need to move to the isolated worker model. All dates come from Microsoft's release metadata and support policy.

VersionRelease typeLatest patch (13 Oct 2026)End of support
.NET 8LTS8.0.32 (SDK 8.0.426)10 Nov 2026
.NET 9STS9.0.21 (SDK 9.0.319)10 Nov 2026
.NET 10LTS10.0.13 (SDK 10.0.402)14 Nov 2028

Why two versions end on the same day

.NET 8 is a long-term support release with three years of support. .NET 9 was a standard-term release that would originally have ended in May 2026. In September 2025, Microsoft extended STS support from 18 to 24 months, starting with .NET 9, which moved its end date to 10 November 2026.

So teams that moved from 8 to 9 to stay current got no extra runway. Both groups have the same deadline and the same destination.

.NET 11 launches at .NET Conf, 10 to 13 November. If it follows the 24-month STS term, it would be supported until around November 2028, about when .NET 10 ends. Microsoft's metadata doesn't list a date for it yet. For most production services, .NET 10 is the conservative target: it is already on its thirteenth patch.

What end of support actually removes

Microsoft's policy has two phases. In the final six months of any release (the "maintenance" phase both versions are in now), only security fixes ship. After the end date, nothing ships.

Three practical consequences:

  • No runtime or ASP.NET Core patches. A vulnerability found in December in code shared between .NET 8 and .NET 10 gets fixed in 10 only.
  • Container images freeze. The dotnet-docker tag lifecycle says unsupported tags can still be pulled but never receive another update, for any reason. That includes the operating system layer, so mcr.microsoft.com/dotnet/aspnet:8.0 will collect Debian CVEs as well as .NET ones.
  • Your scanner loses sight of it. This one is less obvious, and it gets its own section below.

How much patching you stop receiving

Microsoft's release metadata records every CVE fixed in each patch release. Here is what .NET 8 received in 2026:

Month (2026).NET 8 releaseCVEs fixed
January8.0.230
February8.0.241
March8.0.251
April8.0.264
May8.0.274
June8.0.283
July8.0.2917
August8.0.3010
September8.0.315
October8.0.3240

That is 85 CVEs in ten months, against 10 in all of 2025 and 20 in 2024. The more important number is the overlap. Of the 85 CVEs fixed in .NET 8 this year, 84 were also fixed in .NET 10. Nearly every bug lives in code the two versions share.

After 10 November, that overlap works against you. Every .NET 10 security patch is a public diff showing exactly what changed, in code that very likely still exists, unpatched, in .NET 8.

A fair caveat: a CVE count is not a risk score. Many of October's 40 are denial-of-service bugs or apply only to Windows desktop components, and none was known to be exploited when published. The count tells you how often the shared code needs fixing, not how dangerous any one gap is.

Why your scanner goes quiet after end of support

Software composition analysis tools match the versions you run against the affected ranges in advisories. After a version leaves support, Microsoft's advisories mostly stop mentioning it.

You can check this against .NET 6, which went out of support on 12 November 2024. We pulled every GitHub-reviewed advisory for the .NET runtime packages (Microsoft.NETCore.App.Runtime, Microsoft.AspNetCore.App.Runtime, Microsoft.WindowsDesktop.App.Runtime) published after that date. There were 59. Fifty-four list .NET 8 as affected. One lists .NET 6: CVE-2025-24070, which gives a 6.0 range with no patched version.

That doesn't mean .NET 6 was immune to the other 58. It means nobody checked, or nobody wrote it down. A scanner reads "not listed" as "not affected", so a .NET 6 service would have looked cleaner after end of support than before it. Expect .NET 8 and 9 to follow the same pattern from December. Treat "out of support" as a finding in its own right, not as a version with zero CVEs.

What happened at the last two deadlines

The metadata also shows how Microsoft handled the final days of .NET 6 and .NET 7:

  • .NET 6 ended on 12 November 2024. Its last release, 6.0.36, shipped that day and contained no security fixes. The last security release was 6.0.35, a month earlier.
  • .NET 7 ended on 14 May 2024. 7.0.19 shipped that day with two CVE fixes, and then 7.0.20 followed on 28 May with one more (CVE-2024-20672), two weeks after the end date.

So a release on or just after 10 November is possible. Don't plan around one.

Find every place a repository pins .NET 8 or 9

Target frameworks are only one place a version gets pinned. global.json, Dockerfiles and CI workflows pin it too, and missing any of them leaves you building or running on the old version after the project file says net10.0. This script checks all four. It reads end-of-support dates from Microsoft's metadata instead of hard-coding them, so it keeps working for later releases.

#!/usr/bin/env python3
"""Find .NET versions in a repo that are out of support, or will be within N days.
Usage: python3 dotnet_eol_scan.py <repo-dir> [days-ahead=90]"""
import json, re, sys, urllib.request
from datetime import date, timedelta
from pathlib import Path

INDEX = "https://builds.dotnet.microsoft.com/dotnet/release-metadata/releases-index.json"
root = Path(sys.argv[1])
horizon = date.today() + timedelta(days=int(sys.argv[2]) if len(sys.argv) > 2 else 90)

eol = {}  # "8.0" -> date
for r in json.load(urllib.request.urlopen(INDEX))["releases-index"]:
    if r.get("eol-date"):
        eol[r["channel-version"]] = date.fromisoformat(r["eol-date"])

def channel(ver):
    m = re.match(r"(?:net|netcoreapp)?(\d+)\.(\d+)", ver)
    return f"{m[1]}.{m[2]}" if m else None

def risky(ch):
    return ch in eol and eol[ch] <= horizon

SKIP = {".git", "bin", "obj", "node_modules"}
findings = []
def add(path, line, what, ch):
    findings.append((str(path.relative_to(root)), line, f".NET {ch} (EOL {eol[ch]})", what))

for p in root.rglob("*"):
    if not p.is_file() or SKIP & set(p.relative_to(root).parts):
        continue
    text = p.read_text(errors="ignore")
    lines = text.splitlines()
    if p.suffix in {".csproj", ".fsproj", ".vbproj", ".props", ".targets"}:
        is_app = bool(re.search(r'Sdk="Microsoft\.NET\.Sdk\.(Web|Worker|BlazorWebAssembly)"|<OutputType>\s*(Win)?Exe', text)) \
                 or "AzureFunctionsVersion" in text
        for i, l in enumerate(lines, 1):
            m = re.search(r"<TargetFrameworks?>([^<]+)<", l)
            if m:
                tfms = [t.strip() for t in m[1].split(";") if t.strip()]
                chs = {channel(t) for t in tfms if t.startswith("net") and "standard" not in t and not t.startswith("net4")}
                if not chs:
                    continue
                newest = max(chs, key=lambda c: tuple(map(int, c.split("."))))
                # Class libraries are skipped: the app that loads them decides the runtime.
                if risky(newest) and (is_app or p.suffix in {".props", ".targets"}):
                    add(p, i, f"{'app' if is_app else 'shared build'} targets {m[1]}", newest)
            if "Microsoft.NET.Sdk.Functions" in l and horizon >= date(2026, 11, 10):
                findings.append((str(p.relative_to(root)), i, "Functions in-process (EOL 2026-11-10)", "move to the isolated worker model"))
    elif p.name == "global.json":
        m = re.search(r'"version"\s*:\s*"([\d.]+)"', text)
        if m and risky(channel(m[1])):
            n = text[:m.start()].count("\n") + 1
            add(p, n, f"SDK pinned to {m[1]}", channel(m[1]))
    elif p.name.startswith("Dockerfile") or p.suffix == ".dockerfile":
        for i, l in enumerate(lines, 1):
            m = re.search(r"mcr\.microsoft\.com/dotnet/[\w/-]+:(\d+\.\d+)", l)
            if m and risky(m[1]): add(p, i, l.strip(), m[1])
    elif p.suffix in {".yml", ".yaml"}:
        window = 0  # lines left in a "dotnet-version:" block
        for i, l in enumerate(lines, 1):
            if "dotnet-version" in l:
                window = 6
            if window:
                window -= 1
                m = re.search(r"(?:dotnet-version:|^\s*-?)\s*['\"]?(\d+\.\d+)(?:\.x|\.\d+)?['\"]?\s*$", l)
                if m and risky(m[1]): add(p, i, l.strip(), m[1])

for f in sorted(findings):
    print(f"{f[0]}:{f[1]}  {f[2]}  {f[3]}")
print(f"\n{len(findings)} finding(s); horizon {horizon}")
sys.exit(1 if findings else 0)

We ran it against a test repository with a web API on net8.0, a worker on net9.0, an in-process Function, a class library targeting netstandard2.0;net8.0, a service multi-targeting net8.0;net10.0, a pinned global.json, a two-stage Dockerfile and a GitHub Actions workflow:

.github/workflows/ci.yml:7  .NET 8.0 (EOL 2026-11-10)  8.0.x
Dockerfile:1  .NET 9.0 (EOL 2026-11-10)  FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
Dockerfile:6  .NET 8.0 (EOL 2026-11-10)  FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
global.json:3  .NET 8.0 (EOL 2026-11-10)  SDK pinned to 8.0.400
src/Api/Api.csproj:3  .NET 8.0 (EOL 2026-11-10)  app targets net8.0
src/Func/Func.csproj:3  .NET 8.0 (EOL 2026-11-10)  app targets net8.0
src/Func/Func.csproj:7  Functions in-process (EOL 2026-11-10)  move to the isolated worker model
src/Worker/Worker.csproj:4  .NET 9.0 (EOL 2026-11-10)  app targets net9.0

8 finding(s); horizon 2027-01-12

The library and the multi-targeted service are deliberately not flagged. A class library doesn't choose a runtime: the app that loads it does, and a library built for net8.0 runs fine inside a .NET 10 app. Keeping net8.0 in a library's target list is reasonable while your consumers move. The script exits non-zero when it finds something, so it can run as a CI step. It is a pattern scanner, not an MSBuild evaluator, so frameworks set through variables in imported files will slip past it.

For machines rather than repositories, dotnet --list-runtimes shows what is installed. A host can run .NET 10 apps and still have an old 8.0 runtime on disk, and that runtime matters only if something uses it.

A stopgap for hosts you can't redeploy in time

A framework-dependent app built for net8.0 won't start on a machine that only has the .NET 10 runtime. Roll-forward lets it run there without a rebuild. We tested a small net8.0 console app on a machine with the .NET 10.0.13 runtime and no .NET 8:

$ dotnet hello8.dll
You must install or update .NET to run this application.

$ DOTNET_ROLL_FORWARD=Major dotnet hello8.dll
Target: net8.0  Running on: .NET 10.0.13

The same setting is available as <RollForward>Major</RollForward> in the project file or "rollForward": "Major" in runtimeconfig.json. That gets a supported, patched runtime under an app you can't rebuild before the deadline.

It is a bridge, not a migration. The app now runs on a runtime it was never tested against, so every behavioural breaking change in .NET 9 and 10 applies to it at once. Use it for a short list of hosts with a removal date written down, and test each app before you rely on it.

.NET 10 changes worth knowing before you migrate

Microsoft keeps a full list of .NET 10 breaking changes. These are the ones most likely to affect a security-conscious migration:

  • dotnet restore now audits transitive packages. For projects targeting .NET 10, NuGetAuditMode defaults to all, so vulnerable packages pulled in by your dependencies raise warnings NU1901 to NU1904. With TreatWarningsAsErrors, restore fails. Microsoft's breaking-change note shows how to downgrade those warnings, but the better first move is dotnet nuget why <package> to find which direct dependency brings it in, then upgrade that. Our guide to finding vulnerable .NET dependencies covers the commands.
  • Default container images use Ubuntu. If your Dockerfile installs OS packages, check the package names, and expect your container scanner's baseline to change.
  • OpenSSL 1.1.1 or later is required on Unix, and X500DistinguishedName validation is stricter. Both can surface as certificate or TLS failures on older hosts.
  • Azure Functions: the migration guide swaps the Microsoft.NET.Sdk.Functions package for the Microsoft.Azure.Functions.Worker packages and sets FUNCTIONS_WORKER_RUNTIME to dotnet-isolated. This one is a code change, not a version bump, so start it first.

A plan for the next four weeks

  1. Patch now. Install 8.0.32 or 9.0.21 everywhere. The 13 October release fixed 40 or more CVEs per version, including critical ones, and it is the last patch you are guaranteed.
  2. Run the scan across your repositories and list every app it flags, sorted by internet exposure.
  3. Move internet-facing apps and Functions first. The in-process Functions migration takes the longest.
  4. Roll forward only what genuinely can't move, with an owner and a removal date for each.
  5. Record anything left on 8 or 9 after 10 November as a known exception. Don't let a quiet scanner report close it for you.

For broader patterns on retiring runtimes and frameworks, see our guide to managing end-of-life dependencies.

The real deadline

Nothing breaks on 10 November. The apps keep running, the images keep pulling, and the dashboards may even turn greener. What changes is that every later .NET 10 patch tells an attacker where to look in your .NET 8 code, while your own tooling stops telling you anything. The risk builds month by month, and the quiet makes it easy to miss. If you want vulnerable packages, including transitive ones, tracked across every repository and not just the one you happen to be migrating, that is part of what Safeguard SCA does.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.