ASP.NET Core's security middleware is explicit and modular - each protection is a line added to the request pipeline, which means it's genuinely visible in code review whether it's there, but also genuinely easy to have one missing without anything failing at build time. This checklist covers the gaps that show up most in real deployments.
1. Remove the Developer Exception Page in Production
// Program.cs
if (app.Environment.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler("/Error");
app.UseHsts();
}
The developer exception page returns full stack traces, source code snippets, and query details in the response - confirm this branch is actually taking the production path in your deployed environment, not just that the code looks correct, since an unset or misread ASPNETCORE_ENVIRONMENT variable silently leaves the development branch active.
2. Anti-Forgery Tokens Outside MVC View Helpers
Razor's <form> tag helper includes an anti-forgery token automatically. Minimal APIs, Razor Pages without the tag helper, and any hand-built form or fetch-based POST do not get this for free - each needs [ValidateAntiForgeryToken] or an equivalent explicit check.
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult UpdateProfile(ProfileModel model) { /* ... */ }
3. CORS Policy
builder.Services.AddCors(options => {
options.AddPolicy("Default", policy => {
policy.WithOrigins("https://yourapp.com")
.AllowCredentials();
// Never .AllowAnyOrigin() combined with .AllowCredentials() -
// ASP.NET Core throws at startup if you try, but a wildcard
// without credentials is still worth scoping to real origins
});
});
4. Secrets: User Secrets, Key Vault, Never appsettings.json
appsettings.json is meant for non-sensitive configuration. Use the .NET Secret Manager (dotnet user-secrets) for local development and Azure Key Vault or environment variables for deployed environments - and confirm appsettings.json and any appsettings.Production.json aren't accidentally served as static files if your app serves a wwwroot alongside the API.
5. SQL Injection via Raw EF Core / Dapper Queries
// Vulnerable: string interpolation in raw SQL
var orders = context.Orders.FromSqlRaw($"SELECT * FROM Orders WHERE Status = '{status}'");
// Fixed: parameterized, even in raw SQL
var orders = context.Orders.FromSqlInterpolated($"SELECT * FROM Orders WHERE Status = {status}");
// FromSqlInterpolated parameterizes interpolated values automatically -
// FromSqlRaw does not, and is the one that needs manual parameters
6. Cookie and Session Security
builder.Services.ConfigureApplicationCookie(options => {
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
});
7. HSTS
app.UseHsts() is excluded from the default development pipeline (by design, since local HTTPS certificates aren't part of the public trust chain) - confirm it's actually present on the production branch of your startup configuration, not just assumed because it appears somewhere in the file.
8. Dependency Scanning
dotnet list package --vulnerable --include-transitive
Run this in CI, not as a manual pre-release check - transitive dependencies (packages your direct dependencies pull in) are exactly the ones most likely to be overlooked without automation.
9. Rate Limiting
.NET 7+ includes a built-in rate limiting middleware (Microsoft.AspNetCore.RateLimiting) - apply a stricter policy specifically to authentication and password-reset endpoints rather than one global limit sized for normal traffic.
10. Authorization Policies, Not Just [Authorize]
[Authorize] alone confirms authentication, not object-level ownership - the same IDOR gap covered in our IDOR prevention checklist applies here. Use policy-based or resource-based authorization (IAuthorizationService.AuthorizeAsync) for anything scoped to a specific record.
Verifying From the Outside
Try Shieldome's ASP.NET Core security scanner to check headers, exception page exposure, and configuration from outside the application. Create a free account to run your first scan.