Security Headers — HTTP Security Headers and HTTPS Enforcement

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();
Note: HSTS (HTTP Strict Transport Security) tells the browser: “always use HTTPS for this domain, even if the user types http://.” Once a browser receives an HSTS header, it upgrades HTTP → HTTPS client-side for the duration of 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.
Tip: Test your API’s security headers with securityheaders.com — paste your API URL and get a free graded report. Headers like 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.
Warning: Do not add a restrictive 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.

🧠 Test Yourself

An API response includes X-Content-Type-Options: nosniff. A malicious file is uploaded with a .jpg extension but contains JavaScript. The browser downloads it via an API endpoint. What does this header prevent?