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.
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.jsonor 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 withUrl.IsLocalUrl(returnUrl). -
SQL injection: use parameterised queries / EF Core LINQ; never string-concatenate SQL.
-
IP safelist: a small middleware that rejects requests whose
Connection.RemoteIpAddressis not allowed. -
Response headers:
X-Content-Type-Options: nosniff,Referrer-Policy,X-Frame-Options(or CSPframe-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 |
|
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 |
See Security overview.