HTTP security headers instruct browsers how to handle API responses, reducing the attack surface for XSS, clickjacking, MIME sniffing, and other browser-based attacks. While most security headers are more relevant to HTML responses than pure JSON APIs, an API that also serves the Angular app (single-server deployment), or whose responses flow through a browser, benefits significantly. The headers added in this lesson are a baseline that covers the most common browser security policies without breaking Angular clients.
Security Headers Middleware
// ── Custom security headers middleware ────────────────────────────────────
public class SecurityHeadersMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
var headers = context.Response.Headers;
// Prevent MIME type sniffing (browser must use declared content type)
headers["X-Content-Type-Options"] = "nosniff";
// Prevent clickjacking — deny framing by other origins
headers["X-Frame-Options"] = "DENY";
// Remove server version information
headers.Remove("Server");
headers.Remove("X-Powered-By");
headers.Remove("X-AspNet-Version");
// Referrer policy — limit referrer header to same-origin
headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
// Permissions policy — disable unused browser features
headers["Permissions-Policy"] =
"camera=(), microphone=(), geolocation=(), payment=()";
// Content-Security-Policy — for API responses (restrictive)
// APIs typically return JSON — no scripts, styles, or frames needed
headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'";
await next(context);
// HSTS — must be set after response (requires HTTPS context)
if (context.Request.IsHttps)
headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload";
}
}
// ── Register ──────────────────────────────────────────────────────────────
app.UseMiddleware<SecurityHeadersMiddleware>(); // early in pipeline
// ── HTTPS enforcement ─────────────────────────────────────────────────────
builder.Services.AddHsts(opts =>
{
opts.MaxAge = TimeSpan.FromDays(365);
opts.IncludeSubDomains = true;
opts.Preload = true;
});
app.UseHttpsRedirection();
app.UseHsts();
max-age — no HTTP request is ever sent to the server, preventing man-in-the-middle downgrade attacks. With preload: true, you can submit the domain to the HSTS preload list (shipped in browsers), ensuring HTTPS even on first visit. Only add preload if you are 100% committed to HTTPS — removing a domain from the preload list takes months.X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security are low-effort additions that immediately improve the security posture. The report also flags headers that are set to ineffective values. Run this check after every deployment and treat a grade below B as a finding to address.Content-Security-Policy header to your Angular app-serving endpoint without testing thoroughly. A CSP like default-src 'none' on the HTML page that serves the Angular app blocks all scripts, styles, and fonts — Angular will not load at all. For the Angular SPA endpoint, the CSP must permit script-src 'self' and style-src 'self' at minimum. For JSON API endpoints (which return no HTML/scripts), default-src 'none' is correct and safe.CORS Configuration for Production
// ── Production CORS — explicit origins, not wildcard ─────────────────────
builder.Services.AddCors(opts =>
opts.AddPolicy("Production", policy =>
policy
.WithOrigins(
builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
?? ["https://blogapp.com"])
.WithMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.WithHeaders("Authorization", "Content-Type", "X-Correlation-Id")
.AllowCredentials() // for SignalR
.SetPreflightMaxAge(TimeSpan.FromHours(1)))); // cache pre-flight
// ── Never use wildcard (*) with credentials ────────────────────────────────
// WRONG: .AllowAnyOrigin().AllowCredentials() ← browser rejects this combination
// RIGHT: .WithOrigins("https://...").AllowCredentials()
// ── Allowed origins from config (environment-specific) ───────────────────
// appsettings.Production.json:
// { "Cors": { "AllowedOrigins": ["https://blogapp.com", "https://www.blogapp.com"] } }
// appsettings.Development.json:
// { "Cors": { "AllowedOrigins": ["http://localhost:4200"] } }
Common Mistakes
Mistake 1 — Exposing server version headers (reveals attack surface)
❌ Wrong — responses include Server: Kestrel, X-Powered-By: ASP.NET; attackers know exact versions.
✅ Correct — remove server identification headers in the security middleware.
Mistake 2 — Using AllowAnyOrigin with AllowCredentials in production (browser rejects)
❌ Wrong — wildcard CORS with credentials; browser blocks with CORS error; Angular cannot connect.
✅ Correct — explicitly list allowed origins; never combine wildcard with credentials.