Security Hardening

This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14.

This section’s bibliography lists the reference material consulted while preparing these pages.

ASP.NET Core is secure by default in many respects; this page collects the settings and patterns you still have to apply deliberately. See ASP.NET Core security topics.

Data Protection API

Data Protection encrypts small payloads (auth cookies, antiforgery tokens, Protected*Storage, your own short-lived tokens) with a managed key ring.

builder.Services.AddDataProtection()
    .PersistKeysToAzureBlobStorage(blobUri, credential)   // shared store for a web farm
    .ProtectKeysWithAzureKeyVault(keyVaultKeyUri, credential)  // encrypt the keys at rest
    .SetApplicationName("shop");                          // share keys only within this app

public sealed class LinkSigner(IDataProtectionProvider provider)
{
    private readonly IDataProtector _p = provider.CreateProtector("shop.email-links.v1");
    public string Protect(string value) => _p.Protect(value);
    public string Unprotect(string token) => _p.Unprotect(token);
}

Keys rotate automatically (default lifetime 90 days); old keys stay in the ring for decryption. Every instance in a farm must read the same persisted, encrypted key ring and use the same application name. See Data Protection overview.

flowchart LR subgraph Ring["Key ring (persisted + encrypted at rest)"] K1["key 1
expired, decrypt only"] K2["key 2
active: encrypt + decrypt"] K3["key 3
queued for next rotation"] end APP["CreateProtector('purpose')"] --> K2 K2 --> OUT["Protect() -> ciphertext"] IN["Unprotect(ciphertext)"] --> Ring

HTTPS and HSTS

app.UseHsts();                 // Strict-Transport-Security (skip in Development)
app.UseHttpsRedirection();     // 307/308 http -> https

Trust the dev certificate locally with dotnet dev-certs https --trust. Behind a TLS-terminating proxy, add UseForwardedHeaders (see Hosting) so redirects and Request.IsHttps are correct. See Enforce HTTPS in ASP.NET Core.

Secret management

  • Development: User Secrets (dotnet user-secrets set …​) — stored outside the repo, never committed.

  • Production: environment variables, a secrets manager (Azure Key Vault, AWS/GCP equivalents), and managed identity so no credential is stored at all.

  • Never place secrets in appsettings.json or source control. Cross-link Configuration and the Options pattern.

CORS

builder.Services.AddCors(o => o.AddPolicy("spa", p => p
    .WithOrigins("https://app.example.com")
    .WithMethods("GET", "POST")
    .AllowAnyHeader()));

app.UseCors("spa");            // between UseRouting and UseAuthorization
// or per endpoint: app.MapGet(...).RequireCors("spa");  /  [EnableCors("spa")]

Never combine AllowAnyOrigin() with AllowCredentials(). Full treatment: What is CORS?.

Antiforgery / CSRF

The antiforgery token pairs a cookie with a request token so a third-party site cannot forge an authenticated POST.

builder.Services.AddAntiforgery(o => o.HeaderName = "X-CSRF-TOKEN");
app.UseAntiforgery();          // required for Razor Pages, MVC forms, Blazor SSR forms

Razor Pages and MVC <form> tag helpers inject the hidden token automatically; add [AutoValidateAntiforgeryToken] globally so every unsafe verb is checked. For [ApiController] and minimal endpoints that accept browser form posts, validate explicitly with IAntiforgery or the .ValidateAntiforgeryToken() endpoint metadata. Pure token-authenticated APIs (bearer, no cookies) are not exposed to CSRF. See Prevent CSRF attacks.

XSS and Content Security Policy

Razor HTML-encodes @expressions by default — never call Html.Raw on user input. Encode for the right context with HtmlEncoder / JavaScriptEncoder / UrlEncoder. Add a Content Security Policy header to block injected scripts:

app.Use(async (ctx, next) =>
{
    ctx.Response.Headers["Content-Security-Policy"] =
        "default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'none'";
    await next(ctx);
});

Other hardening and the OWASP map

  • Open redirect: use LocalRedirect(returnUrl) (throws on absolute URLs) or validate with Url.IsLocalUrl(returnUrl).

  • SQL injection: use parameterised queries / EF Core LINQ; never string-concatenate SQL.

  • IP safelist: a small middleware that rejects requests whose Connection.RemoteIpAddress is not allowed.

  • Response headers: X-Content-Type-Options: nosniff, Referrer-Policy, X-Frame-Options (or CSP frame-ancestors).

  • Rate limiting as an abuse control — see Performance and caching.

  • GDPR cookie consent: the consent-cookie API gates non-essential cookies until the user agrees.

OWASP Top 10 ASP.NET Core mitigation

Broken access control

Policy-based authorization; fallback policy; resource-based checks

Cryptographic failures

Data Protection API; HTTPS/HSTS; Key Vault

Injection

EF Core / parameterised SQL; model binding + validation; output encoding

Insecure design

Threat-model; secure defaults; least privilege in DI and DB accounts

Security misconfiguration

Environment-specific config; no secrets in source; disable the dev exception page in prod

Vulnerable components

dotnet list package --vulnerable; Dependabot/NuGet audit

Auth failures

ASP.NET Core Identity lockout + 2FA; OIDC + PKCE

Data integrity failures

Signed tokens; antiforgery; SRI for third-party scripts

Logging & monitoring failures

Structured logging; health checks; OpenTelemetry (see Observability)

SSRF

Validate and allow-list outbound URLs; block link-local/metadata addresses