How to Protect ASP.NET Core APIs Against Insecure HTTP Response Headers

Search for a command to run...

No comments yet. Be the first to comment.
Tool calling is the moment an LLM stops being a text generator and starts touching your systems. Securing LLM tool calling in ASP.NET Core is therefore not really an AI problem - it is an authorizatio

Retrieval-Augmented Generation lives or dies on one unglamorous step, and it is not the model or the vector database. It is chunking. When you get chunking documents for RAG in .NET wrong, the model r

Most teams reach for caching the moment an API gets slow, wire up Redis or output caching, and move on. But there is an older, lighter mechanism built into HTTP itself that solves two problems at once

If you shipped an AI agent in .NET over the last two years, there is a good chance it runs on Semantic Kernel. That was the right call at the time. But with Microsoft Agent Framework reaching GA in Ap

The first time an AI feature I shipped went down in production, nothing in my code had changed. The model provider had a bad afternoon: a wave of 429 Too Many Requests, then a stretch of 503s, and eve

Coding Droplets
298 posts
Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.
From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.
Most ASP.NET Core developers spend significant effort securing authentication flows, validating inputs, and authorizing requests — and then ship an API that leaks its server version in every response, invites browsers to sniff MIME types, and leaves clients vulnerable to a simple downgrade attack. HTTP response headers are not glamorous, but they are the outermost defence layer of your API surface, and misconfigured or absent security headers sit consistently inside the OWASP Top 10 as part of Security Misconfiguration (A05). Getting them right is one of the fastest, highest-leverage security improvements any .NET team can make. For developers who want to see these defences wired into a complete, production-ready API — alongside authentication, global error handling, and structured logging already connected — the full implementation with annotated source code is available on Patreon.
Most documentation on HTTP security headers focuses on browser-rendered web applications. APIs seem different — your API doesn't serve HTML, so why would Content-Security-Policy matter?
The risk surface is broader than most teams expect:
Server: Kestrel in every response, and many hosting stacks append X-Powered-By or X-AspNet-Version. Attackers use this information to identify vulnerable server versions within seconds using automated tools.SameSite semantics and X-Frame-Options even when the primary client is a SPA receiving JSON.The OWASP Secure Headers Project defines a baseline set of headers that every production API should include. For ASP.NET Core teams, applying them consistently across every endpoint is a middleware concern — not a per-controller one.
HSTS instructs the browser that this domain must only be reached over HTTPS — and for the duration specified, the browser enforces this policy itself before making any request.
Threat mitigated: SSL stripping and man-in-the-middle downgrade attacks. Without HSTS, an attacker on the same network can intercept the first plaintext HTTP request and silently redirect it before the server can issue a redirect response.
ASP.NET Core ships UseHsts() as a built-in middleware. For production, configure a max-age of at least one year and include includeSubDomains once you have verified HTTPS is working correctly across all subdomains.
Critical operational note: Never start HSTS at a high max-age value until you are certain that HTTPS is working correctly across every subdomain. Start with a low value, verify thoroughly, then increase. A broken TLS certificate combined with a one-year HSTS header locks all browser clients out until the header duration expires — and headers cannot be expired if browsers cannot reach the server over HTTPS to receive an updated value.
Threat mitigated: MIME sniffing. Browsers can attempt to infer the content type of a response by inspecting its byte content, a behaviour called MIME sniffing. An attacker who can influence content your API returns — for example, a user-uploaded file served back as a download — can exploit this to cause a browser to execute it as a script or render it as markup.
The value nosniff is a one-liner with no downside. There is no scenario in which omitting it is defensible. Every API response should include it.
Threat mitigated: Clickjacking. Loading your API's Swagger UI or an authenticated management portal inside a hidden <iframe> is a classic vector for tricking users into issuing requests they did not intend to make.
For pure JSON APIs, DENY is the correct default. If your application includes rendered HTML — Razor pages, Blazor components, or Swagger UI — consider SAMEORIGIN only for pages that are genuinely expected to be embedded in a same-origin frame. Note that X-Frame-Options: DENY is also expressed more expressively via the CSP frame-ancestors directive, and modern applications should prefer CSP for this control while keeping X-Frame-Options for older browser compatibility.
CSP is the most powerful and most complex of the security headers. It defines what resources a browser is permitted to load and execute in the context of your origin.
For a pure JSON API with no Swagger UI in production, a minimal policy is:
Content-Security-Policy: default-src 'self'; object-src 'none'; frame-ancestors 'none'
The moment you add Swagger or a rendered management portal, the policy must expand to allow the assets those tools load. The key discipline is keeping the production policy as tight as possible and scoping any relaxations to specific route prefixes rather than applying them globally.
What frame-ancestors 'none' does: It is the CSP-native equivalent of X-Frame-Options: DENY. Both should be set for defence-in-depth — older browsers honour X-Frame-Options while modern browsers honour the CSP directive.
Threat mitigated: URL leakage via the Referer header. When a user follows a link from your application to an external resource, the browser attaches the current page URL as the Referer header. For APIs handling authenticated workflows, those URLs can include user identifiers, session tokens, or internal resource paths that should never leave your origin.
no-referrer is the strictest option and is appropriate for API endpoints. For applications with analytics requirements, strict-origin-when-cross-origin sends only the origin (not the full URL path) on cross-origin requests, which is a pragmatic balance.
Threat mitigated: Feature policy misuse. This header lets you explicitly disable browser features your API has no legitimate reason to access. Disabling geolocation, microphone, and camera for an API that handles financial records or healthcare data is a meaningful defence-in-depth control, especially when the API is served alongside JavaScript-heavy admin interfaces.
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()
An empty value () means the feature is denied for all origins, including the page's own origin.
These headers actively help attackers by disclosing your technology stack, and neither of them provides any benefit to legitimate clients.
Server: Kestrel — Disable in Kestrel server options. This is separate from middleware header assignment because Kestrel emits this header at the transport layer, not the application layer.
X-Powered-By: ASP.NET — Added by IIS and some reverse proxy configurations. Remove it via web.config customHeaders removal, or suppress it in your middleware pipeline by calling context.Response.Headers.Remove("X-Powered-By").
X-XSS-Protection — Deprecated and removed from all living browser standards. Worse, in older Internet Explorer versions, certain values of this header can actually introduce vulnerabilities rather than mitigate them. Do not include it. If legacy configuration in your pipeline is emitting it, remove it.
Expect-CT — Deprecated following the retirement of the Certificate Transparency enforcement model in this header form. No current browser uses it. Do not add it; remove it if present.
P3P — An obsolete privacy policy header that no browser has honoured for over a decade. Remove if present in legacy configurations.
All security headers belong in a single, centralized middleware that runs before all other application middleware. Placing header logic after routing, authentication, or authorization middleware means there are application code paths that can emit a response before your headers are set — including error responses from middleware earlier in the pipeline.
The correct position is the very first app.Use(...) call in Program.cs, before app.UseHsts(), app.UseAuthentication(), app.UseRouting(), and everything else.
// Must be first — before UseHsts, UseRouting, UseAuthentication
app.Use(async (context, next) =>
{
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
context.Response.Headers["Referrer-Policy"] = "no-referrer";
context.Response.Headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()";
context.Response.Headers["Content-Security-Policy"] =
"default-src 'self'; object-src 'none'; frame-ancestors 'none'";
context.Response.Headers.Remove("X-Powered-By");
await next();
});
app.UseHsts(); // Built-in HSTS middleware — call separately, not inside the block above
UseHsts() is called separately because ASP.NET Core ships it as a first-class middleware with its own configuration options (HstsOptions) and because it handles the preload subdomain logic through its own pipeline.
To configure HSTS for production:
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
options.Preload = true; // Only after verifying all subdomains work over HTTPS
});
And disable the default Server header at the Kestrel level:
builder.WebHost.ConfigureKestrel(options =>
{
options.AddServerHeader = false;
});
The anti-pattern that appears most often in enterprise ASP.NET Core codebases is header assignment scattered across multiple places — set on some middleware, duplicated in a filter, missing from background tasks that return HTTP responses, and absent from feature-branch endpoints merged without security review. The symptom is an inconsistent audit result: some endpoints score an A on securityheaders.com while others return no security headers at all.
The second common anti-pattern is the Server: Kestrel header surviving into production because AddServerHeader = false was never set. Every response your API returns, including health check probes, Swagger spec requests, and error responses from the exception handler, advertises your server version to anyone running a scraper.
After each deployment:
curl -I https://your-api.com/health — shows full response headers in a terminal; useful for quick spot checksWebApplicationFactory — assert the presence of required headers in your test suite so configuration regressions are caught before productionThe most reliable approach is a dedicated integration test that makes a request to each major endpoint group and asserts on response.Headers values. If security headers are not part of your regression suite, there is nothing preventing a future configuration change from silently removing them.
For guidance on the authentication hardening that sits behind this header layer, see How to Protect ASP.NET Core APIs Against Broken Authentication. For the full picture on cross-origin controls and how CORS policy interacts with your origin strategy, the ASP.NET Core CORS Policy Enterprise Decision Guide is a natural companion read.
app.Use(...) for security headers is the first middleware in the pipelineAddServerHeader = false is configured in Kestrel optionsX-Powered-By is removed from all responsesX-Content-Type-Options: nosniff is applied to every responseX-Frame-Options: DENY is set (or SAMEORIGIN with explicit justification)Referrer-Policy is configured — no-referrer for pure APIsContent-Security-Policy is defined, and tightest policy applies in productionPermissions-Policy disables all browser features the application has no use forX-XSS-Protection is explicitly not included and removed if emitted by legacy configExpect-CT is explicitly not includedapp.UseHsts() is called and MaxAge is at least 365 days in productionHstsOptions.Preload = true is set only after verifying all subdomains serve HTTPS correctlyYes. Pure JSON APIs still communicate with browser clients via fetch or AJAX calls from SPAs, mobile web views, or Swagger UI. Missing security headers expose those browser contexts to MIME sniffing, clickjacking via iframes, and downgrade attacks. Additionally, headers like Server and X-Powered-By disclose technology stack details to any attacker running a basic network probe against your endpoint — regardless of whether the client is a browser.
NetEscapades.AspNetCore.SecurityHeaders by Andrew Lock is the most widely used library for this purpose. It provides a fluent API for declaring all standard headers with sensible defaults and supports per-endpoint policy overrides. Using it reduces boilerplate and makes header policy changes traceable through code review. That said, understanding what each header does before delegating to a library is important — libraries apply defaults that may conflict with specific API behaviour, such as CSP directives that break Swagger in development.
UseHttpsRedirection() issues a 301 redirect from HTTP to HTTPS at the application layer for any request that arrives on plain HTTP. HSTS operates at the browser layer — once the browser receives the HSTS header, it refuses to make HTTP requests to that origin at all, preventing the initial plaintext request from ever leaving the client. Both should be used together: UseHttpsRedirection() handles the first-time upgrade; HSTS prevents every future request from even attempting HTTP.
Yes. A restrictive CSP that blocks unsafe-inline scripts will break Swagger UI, which loads inline scripts and styles. The correct solution is to keep the strict production CSP and disable Swagger in production entirely. Swagger UI should not be exposed in production environments — if it is, scope a permissive policy only to the Swagger route group and never use unsafe-eval. Separate development and production CSP configurations using environment checks in Program.cs.
Review your headers when: (1) you upgrade to a new major ASP.NET Core version, since default middleware behaviour can change what gets emitted; (2) you add new third-party CDN scripts or fonts, which requires expanding CSP; (3) the OWASP Secure Headers Project publishes recommendation updates; and (4) a penetration test or annual security audit is run. At a minimum, include a securityheaders.com scan in your staging deployment pipeline so header regressions are caught before production promotion.
Removing Server: Kestrel does not make an API secure in isolation, but it raises the attacker's cost. Without a disclosed server version, an attacker cannot immediately look up known CVEs for your exact runtime version — they must probe more actively to fingerprint your stack. The control is sometimes dismissed as security through obscurity, but the correct framing is that information disclosure prevention is a distinct, additive layer that costs nothing to implement.
Even internal microservices benefit from X-Content-Type-Options: nosniff and X-Frame-Options: DENY — both are zero-cost and protect against classes of attack that are not limited to public internet exposure. Server header removal is equally worthwhile — internal network visibility means internal threats and compromised tools can still use server version information for lateral movement. HSTS is less critical if the service only receives traffic from other services over a controlled internal TLS channel, but retaining it is harmless. CSP is most important for services that serve rendered content even internally.