Learn ASP.NET Core security best practices for 2026: stop XSS, CSRF, SQL injection & more with real C# code. Secure your web app today.
Web applications are attacked constantly, and ASP.NET Core security is something every .NET developer needs to get right before shipping to production. The good news: ASP.NET Core ships with strong defaults for authentication, authorization, data protection, and request validation. The bad news: those defaults only help if you understand them, keep them switched on, and don't accidentally undo them with a well-meaning configuration change. This guide walks through the ASP.NET Core security best practices that matter most in 2026, with runnable C# examples and an explanation of why each one matters.
We'll cover the OWASP Top 10 threats as they apply to .NET, from SQL injection and cross-site scripting (XSS) to broken authentication and misconfigured security headers, and finish with a checklist you can run against your own project.
Why ASP.NET Core Security Matters More in 2026
Three trends make securing your web app harder than it was a few years ago:
- Automated attacks: Bots scan every public IP for known vulnerabilities within minutes of deployment. A misconfigured endpoint gets found whether or not anyone is "targeting" you.
- API-first architectures: Most ASP.NET Core apps now expose JSON APIs consumed by SPAs and mobile apps, which changes how authentication, CSRF, and rate limiting must be handled.
- Compliance pressure: GDPR (UK/EU), CCPA (USA), and PIPEDA (Canada) all carry real penalties for data breaches, so secure defaults are a business requirement, not a nice-to-have.
1. Enforce HTTPS and HSTS Everywhere
Everything else depends on transport security. Without HTTPS, cookies, tokens, and form data travel in plain text. ASP.NET Core makes this simple:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(365);
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.Run();
Why: UseHttpsRedirection bounces HTTP requests to HTTPS, and HSTS tells browsers to never even attempt HTTP for your domain again. Together they defeat SSL-stripping attacks on public Wi-Fi. Only enable HSTS outside development, because the browser caches it and you can't easily undo it on localhost.
2. Prevent SQL Injection in C#
SQL injection is still one of the most common breaches, and it's entirely preventable. The rule: never concatenate user input into a SQL string.
// ❌ Vulnerable: user input becomes part of the query text
var sql = $"SELECT * FROM Users WHERE Email = '{email}'";
// ✅ Safe with Entity Framework Core (LINQ is always parameterized)
var user = await db.Users
.FirstOrDefaultAsync(u => u.Email == email);
// ✅ Safe with raw SQL in EF Core (interpolated values become parameters)
var users = await db.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Email = {email}")
.ToListAsync();
// ✅ Safe with Dapper
var user2 = await connection.QueryFirstOrDefaultAsync<User>(
"SELECT * FROM Users WHERE Email = @Email",
new { Email = email });
Why: Parameterized queries send the query and the data to the database separately, so an input like ' OR 1=1 -- is treated as a literal string, not as SQL. Note the subtle trap: FromSqlInterpolated is safe, but FromSqlRaw with string interpolation is not.
3. Stop Cross-Site Scripting (XSS)
Razor encodes output by default, which blocks most XSS. You break that protection the moment you use @Html.Raw() or build HTML strings manually.
@* Safe: Razor HTML-encodes automatically *@
<p>@Model.Comment</p>
@* Dangerous unless the content was sanitized server-side *@
<div>@Html.Raw(Model.RichTextComment)</div>
If you must render user-supplied HTML (rich-text editors, for example), sanitize it with a library such as HtmlSanitizer:
using Ganss.Xss;
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedTags.Clear();
sanitizer.AllowedTags.UnionWith(new[] { "p", "b", "i", "ul", "li", "a" });
sanitizer.AllowedAttributes.Clear();
sanitizer.AllowedAttributes.Add("href");
string safeHtml = sanitizer.Sanitize(userInput);
Add a Content Security Policy header as a second layer so that even if a script sneaks through, the browser refuses to run inline or third-party JavaScript (see section 6).
4. ASP.NET Core CSRF Protection
Cross-site request forgery tricks a logged-in user's browser into submitting a request they never intended. Razor Pages and MVC form tag helpers emit anti-forgery tokens automatically; you just need to validate them:
// Program.cs – validate on all unsafe HTTP methods globally
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
// Minimal APIs (.NET 8+) – enable anti-forgery middleware
builder.Services.AddAntiforgery();
app.UseAntiforgery();
app.MapPost("/profile", async (HttpContext ctx, IAntiforgery af) =>
{
await af.ValidateRequestAsync(ctx); // throws if token missing/invalid
return Results.Ok();
});
Why: CSRF only works when authentication rides on a cookie the browser attaches automatically. Pure token-based APIs (bearer tokens in an Authorization header) are not vulnerable, but any cookie-authenticated endpoint is. Also set SameSite=Strict or Lax on your auth cookies for defense in depth.
5. Secure ASP.NET Core Authentication and Authorization
Broken access control tops the OWASP list. Follow these rules:
- Use ASP.NET Core Identity or an external identity provider (Microsoft Entra, Auth0, Okta). Never write your own password hashing; Identity uses PBKDF2 with per-user salts by default.
- Deny by default. Require authorization globally and opt out with
[AllowAnonymous]. - Check ownership, not just login. Being authenticated doesn't mean a user may see another user's order.
builder.Services.AddAuthorizationBuilder()
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build())
.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));
builder.Services.ConfigureApplicationCookie(o =>
{
o.Cookie.HttpOnly = true;
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.Cookie.SameSite = SameSiteMode.Lax;
o.ExpireTimeSpan = TimeSpan.FromMinutes(30);
o.SlidingExpiration = true;
});
// Resource-based check inside an endpoint
app.MapGet("/orders/{id:int}", async (int id, ClaimsPrincipal user, AppDbContext db) =>
{
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
var order = await db.Orders.FindAsync(id);
if (order is null || order.OwnerId != userId)
return Results.NotFound(); // don't leak existence with 403
return Results.Ok(order);
});
For JWT-based APIs, validate issuer, audience, lifetime, and signing key, and keep access tokens short-lived (5–15 minutes) with refresh tokens stored securely.
6. Add ASP.NET Core Security Headers
Security headers are the cheapest defense you'll ever deploy. A small middleware sets them on every response:
app.Use(async (context, next) =>
{
var h = context.Response.Headers;
h["X-Content-Type-Options"] = "nosniff";
h["X-Frame-Options"] = "DENY";
h["Referrer-Policy"] = "strict-origin-when-cross-origin";
h["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
h["Content-Security-Policy"] =
"default-src 'self'; script-src 'self'; object-src 'none'; " +
"frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
h.Remove("Server");
await next();
});
Why: CSP blocks injected scripts, X-Frame-Options stops clickjacking, and nosniff prevents browsers from executing a disguised upload as JavaScript. Test your configuration at securityheaders.com after deploying.
7. Validate All Input and Prevent Over-Posting
Bind requests to dedicated DTOs rather than EF entities so attackers can't set fields like IsAdmin through a form:
public record UpdateProfileRequest(
[property: Required, StringLength(100)] string DisplayName,
[property: EmailAddress] string Email);
app.MapPut("/profile", async (UpdateProfileRequest req, AppDbContext db, ClaimsPrincipal user) =>
{
if (!MiniValidator.TryValidate(req, out var errors))
return Results.ValidationProblem(errors);
var entity = await db.Users.FindAsync(user.FindFirstValue(ClaimTypes.NameIdentifier));
entity!.DisplayName = req.DisplayName;
entity.Email = req.Email;
await db.SaveChangesAsync();
return Results.NoContent();
});
Also limit request body size (options.Limits.MaxRequestBodySize in Kestrel) and validate uploaded file types by content, not by extension.
8. Rate Limiting to Block Brute Force and Abuse
.NET 7+ includes built-in rate limiting middleware. Apply it to login and password-reset endpoints at a minimum:
using System.Threading.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddPolicy("login", ctx =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(1)
}));
});
app.UseRateLimiter();
app.MapPost("/login", LoginHandler).RequireRateLimiting("login");
9. Protect Secrets and Configuration
- Never commit connection strings or API keys. Use User Secrets in development and Azure Key Vault, AWS Secrets Manager, or environment variables in production.
- Never run with
app.UseDeveloperExceptionPage()in production; stack traces reveal file paths, versions, and query text. - Persist Data Protection keys (
AddDataProtection().PersistKeysToAzureBlobStorage(...)) when running multiple instances, or cookies and anti-forgery tokens will break across nodes.
// Development: dotnet user-secrets set "ConnectionStrings:Default" "..."
// Production: read from Key Vault
builder.Configuration.AddAzureKeyVault(
new Uri("https://my-vault.vault.azure.net/"),
new DefaultAzureCredential());
10. Keep Dependencies Patched
Vulnerable NuGet packages are a supply-chain risk. Run dotnet list package --vulnerable --include-transitive in CI and fail the build on high-severity results. Stay on a supported .NET LTS release (.NET 10 in 2026) and enable Dependabot or Renovate for automatic update PRs.
Common ASP.NET Core Security Pitfalls
- Wrong middleware order:
UseAuthentication()must come beforeUseAuthorization(), and both must come afterUseRouting()and beforeMapControllers(). Wrong order silently disables authorization. - CORS with
AllowAnyOrigin()plus credentials: ASP.NET Core refuses this combination for a reason; specify exact origins instead. - Returning entities directly from APIs: leaks password hashes, internal IDs, and navigation properties. Map to DTOs.
- Logging sensitive data: never log passwords, tokens, or full card numbers. Use structured logging with redaction.
- Trusting
X-Forwarded-Forblindly: configureForwardedHeadersOptions.KnownProxiesso rate limiting and audit logs can't be spoofed.
ASP.NET Core Security Checklist for 2026
- ✅ HTTPS redirection and HSTS enabled in production
- ✅ All database access parameterized (EF Core LINQ,
FromSqlInterpolated, Dapper parameters) - ✅ No unsanitized
Html.Raw; CSP header deployed - ✅ Anti-forgery validation on every cookie-authenticated POST/PUT/DELETE
- ✅ Fallback authorization policy requires authentication; resource ownership checked
- ✅ Security headers middleware in place
- ✅ DTOs with validation attributes; no over-posting to entities
- ✅ Rate limiting on login, registration, and password reset
- ✅ Secrets in Key Vault / environment, not in source control
- ✅
dotnet list package --vulnerableruns in CI
Conclusion
ASP.NET Core security is mostly about not disabling the protections the framework already gives you, then layering a few cheap, high-value defenses on top: HTTPS with HSTS, parameterized queries, output encoding plus CSP, anti-forgery tokens, deny-by-default authorization, security headers, rate limiting, and proper secret management. None of these take more than an hour to implement, and together they eliminate the vast majority of real-world attacks against .NET web apps.
Key takeaways:
- Transport security first: nothing else matters if traffic is readable.
- Never build SQL or HTML from raw user input.
- Authorize every request and verify resource ownership, not just login state.
- Defense in depth: headers, CSP, rate limiting, and patched dependencies catch what slips past the first line.
- Audit your app against the checklist above before every release.
Apply these ASP.NET Core security best practices today, and your web application will be far harder to break than the next target the bots find.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment