Security Headers and API Hardening in ASP.NET Core
Secure your ASP.NET Core API with security headers, CORS policy, HTTPS enforcement, rate limiting, and the defense-in-depth patterns that harden APIs against common web attacks.
Defense in Depth for APIs
Authentication proves who you are. Authorization decides what you can do. Security hardening limits what an attacker can do even with a valid token. These are independent layers — all three are required.
Attack surface reduction:
HTTPS: intercept-proof transport
Security headers: browser-level protections
CORS: cross-origin request restrictions
Rate limiting: brute force and DDoS mitigation
Input validation: injection attack prevention
Sensitive data: no PII in logs or error responsesHTTPS Enforcement
// Program.cs
if (!app.Environment.IsDevelopment())
{
app.UseHsts(); // HTTP Strict Transport Security header
// Tells browsers: only connect via HTTPS for the next year
// Prevents SSL stripping attacks
}
app.UseHttpsRedirection(); // redirect HTTP → HTTPS (add in all environments)// Force HTTPS in URL generation
builder.Services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
options.HttpsPort = 443;
});Security Headers Middleware
// Middleware/SecurityHeadersMiddleware.cs
public sealed class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext ctx)
{
var headers = ctx.Response.Headers;
// Prevent browsers from MIME-sniffing the content type
headers["X-Content-Type-Options"] = "nosniff";
// Deny framing (clickjacking protection)
headers["X-Frame-Options"] = "DENY";
// Block reflected XSS attacks (legacy browsers)
headers["X-XSS-Protection"] = "1; mode=block";
// Control what info is sent in Referer header
headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
// Content Security Policy — for APIs, restrict to nothing
headers["Content-Security-Policy"] = "default-src 'none'";
// Remove server information leakage
headers.Remove("Server");
headers.Remove("X-Powered-By");
await _next(ctx);
}
}
// Program.cs
app.UseMiddleware<SecurityHeadersMiddleware>();CORS Policy
// Program.cs — define named CORS policies
builder.Services.AddCors(options =>
{
// Production policy — strict
options.AddPolicy("ProductionPolicy", policy =>
policy.WithOrigins("https://app.systemforge.com", "https://portal.systemforge.com")
.WithMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.WithHeaders("Authorization", "Content-Type", "X-Request-Id")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10)));
// Development policy — permissive
options.AddPolicy("DevelopmentPolicy", policy =>
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
// Apply
app.UseCors(app.Environment.IsDevelopment() ? "DevelopmentPolicy" : "ProductionPolicy");Production issue I've seen: A team set
AllowAnyOrigin().AllowCredentials()— ASP.NET Core throws an exception for this combination because it violates the CORS spec (credentials require explicit origins). They got an unhelpful 500 error at startup. The fix is to always useWithOrigins()whenAllowCredentials()is needed.
Rate Limiting (.NET 7+)
// Program.cs
builder.Services.AddRateLimiter(options =>
{
// Fixed window: 100 requests per minute per IP
options.AddFixedWindowLimiter("global", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 100;
opt.QueueLimit = 0;
opt.QueueProcessingOrder = QueueProcessingOrder.NewestFirst;
});
// Sliding window: 10 login attempts per 15 minutes per IP
options.AddSlidingWindowLimiter("auth", opt =>
{
opt.Window = TimeSpan.FromMinutes(15);
opt.SegmentsPerWindow = 3;
opt.PermitLimit = 10;
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
app.UseRateLimiter();
// Apply per endpoint
app.MapPost("/auth/login", LoginHandler)
.RequireRateLimiting("auth");
app.MapGet("/patients", GetPatients)
.RequireRateLimiting("global");Sensitive Data in Error Responses
// Never expose stack traces or internal errors to clients
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
// Remove stack trace in production
if (!ctx.HttpContext.RequestServices
.GetRequiredService<IHostEnvironment>().IsDevelopment())
{
ctx.ProblemDetails.Extensions.Remove("exception");
ctx.ProblemDetails.Extensions.Remove("stackTrace");
}
// Add correlation ID for support
ctx.ProblemDetails.Extensions["traceId"] =
ctx.HttpContext.TraceIdentifier;
};
});API Key Authentication for Service-to-Service
// For internal service-to-service calls (not user-facing)
public sealed class ApiKeyAuthMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _config;
public ApiKeyAuthMiddleware(RequestDelegate next, IConfiguration config)
=> (_next, _config) = (next, config);
public async Task InvokeAsync(HttpContext ctx)
{
if (!ctx.Request.Headers.TryGetValue("X-Api-Key", out var key) ||
key != _config["InternalApiKey"])
{
ctx.Response.StatusCode = 401;
return;
}
await _next(ctx);
}
}Preventing Sensitive Data Leakage
Never log:
✗ Passwords (even hashed — the hash is a credential)
✗ JWT tokens (a logged token can be replayed)
✗ Refresh tokens
✗ Patient names / MRN / DOB in default log output
✗ Credit card / SSN data
Use destructuring with allow-listing:
Log PatientId (Guid) — not Patient (entire object)
Log UserId — not Email
Log RequestPath — not request body (may contain passwords)Red Flag / Green Answer
Red Flag: "Our API has AllowAnyOrigin() in production because the frontend team kept having CORS issues during development."
Any website can make cross-origin requests to your API with the user's credentials. An attacker's site can silently call your authenticated API endpoints from the user's browser. This completely bypasses CORS protection.
Green Answer:
WithOrigins("https://app.yourcompany.com")in production. UseAllowAnyOrigin()only in a development-only policy that is not applied in production. Fix CORS issues by explicitly listing allowed origins — not by disabling CORS.
Key Takeaway
Authentication identifies users; security headers and hardening limit what attackers can do even when they have valid tokens. The production checklist: HTTPS enforcement, security response headers, strict CORS with explicit origins, rate limiting on auth endpoints, no sensitive data in error responses. Each layer is independent — adding rate limiting does not replace CORS, and HTTPS does not replace authentication.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.