How to Protect ASP.NET Core APIs Against Insecure HTTP Response Headers
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.
Why Security Headers Matter Specifically for APIs
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:
- Clients that render API responses โ admin dashboards, Swagger UIs, and embedded web views all render content sourced from your API. A missing or permissive CSP in those contexts opens the door to XSS.
- Browser-to-API calls via AJAX or fetch โ without HSTS, a client on HTTP can be silently downgraded, exposing credentials and tokens in transit to a network attacker.
- Information disclosure via uncleaned headers โ by default, ASP.NET Core exposes
Server: Kestrelin every response, and many hosting stacks appendX-Powered-ByorX-AspNet-Version. Attackers use this information to identify vulnerable server versions within seconds using automated tools. - Cross-origin attack vectors โ APIs that set cookies or handle session tokens need
SameSitesemantics andX-Frame-Optionseven 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.
What Each Security Header Does (And Why It Belongs on Your API)
Strict-Transport-Security (HSTS)
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.
X-Content-Type-Options
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.
X-Frame-Options
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.
Content-Security-Policy (CSP)
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.
Referrer-Policy
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.
Permissions-Policy
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.
The Headers You Must Remove
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").
Headers You Should Explicitly Omit
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.
How to Implement Security Headers Correctly
Where the Middleware Must Live
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 Vulnerable Anti-Pattern
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.
How to Verify Your Headers Are Set Correctly
After each deployment:
- securityheaders.com โ paste your API's base URL and get an instant graded report
- OWASP ZAP passive scan โ integrates into CI pipelines and automatically flags missing headers during integration testing
curl -I https://your-api.com/healthโ shows full response headers in a terminal; useful for quick spot checks- Integration tests with
WebApplicationFactoryโ assert the presence of required headers in your test suite so configuration regressions are caught before production
The 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.
Defence-in-Depth Checklist
-
app.Use(...)for security headers is the first middleware in the pipeline -
AddServerHeader = falseis configured in Kestrel options -
X-Powered-Byis removed from all responses -
X-Content-Type-Options: nosniffis applied to every response -
X-Frame-Options: DENYis set (orSAMEORIGINwith explicit justification) -
Referrer-Policyis configured โno-referrerfor pure APIs -
Content-Security-Policyis defined, and tightest policy applies in production -
Permissions-Policydisables all browser features the application has no use for -
X-XSS-Protectionis explicitly not included and removed if emitted by legacy config -
Expect-CTis explicitly not included -
app.UseHsts()is called andMaxAgeis at least 365 days in production -
HstsOptions.Preload = trueis set only after verifying all subdomains serve HTTPS correctly - Security headers are asserted in at least one integration test in the CI pipeline
- securityheaders.com scan returns at least an A rating in the staging environment
Frequently Asked Questions
Do HTTP security headers matter for JSON APIs with no browser front end?
Yes. 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.
Is there a NuGet package that handles all HTTP security headers automatically?
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.
What is the difference between HSTS and the HTTPS redirection middleware?
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.
Can a strict Content-Security-Policy break my Swagger UI?
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.
How often should the security headers configuration be reviewed?
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.
Does removing the Server header provide meaningful security?
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.
Which security headers are safe to skip for a microservice that only services internal traffic?
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.




