Skip to main content

ASP.NET Core Security Best Practices 2026: Protect Your App

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 before UseAuthorization(), and both must come after UseRouting() and before MapControllers(). 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-For blindly: configure ForwardedHeadersOptions.KnownProxies so 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 --vulnerable runs 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.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...