This article shows how to implement security headers for an application supporting an API and a swagger UI created from a open API in .NET 9. The security headers are implemented using the NetEscapades.AspNetCore.SecurityHeaders Nuget packages from Andrew Lock.
Code: https://github.com/damienbod/WebApiOpenApi
Deploying a web application which supports both an API and a UI have different levels of security requirements. The Swagger UI is created from an Open API specification and uses inline Javascript with no hashes or nonces which requires weak security definitions. The API has no UI and can use the maximum security header definitions. It can be locked down as much as possible against the typical web UI attacks.
The API endpoints can be secured using a definition with strict security headers and a lot of browser features locked down.
public static class SecurityHeadersDefinitionsAPI
{
private static HeaderPolicyCollection? policy;
public static HeaderPolicyCollection GetHeaderPolicyCollection(bool isDev)
{
// Avoid building a new HeaderPolicyCollection on every request for performance reasons.
// Where possible, cache and reuse HeaderPolicyCollection instances.
if (policy != null) return policy;
policy = new HeaderPolicyCollection()
.AddFrameOptionsDeny()
.AddContentTypeOptionsNoSniff()
.AddReferrerPolicyStrictOriginWhenCrossOrigin()
.AddCrossOriginOpenerPolicy(builder => builder.SameOrigin())
.AddCrossOriginEmbedderPolicy(builder => builder.RequireCorp())
.AddCrossOriginResourcePolicy(builder => builder.SameOrigin())
.RemoveServerHeader()
.AddPermissionsPolicyWithDefaultSecureDirectives();
policy.AddContentSecurityPolicy(builder =>
{
builder.AddObjectSrc().None();
builder.AddBlockAllMixedContent();
builder.AddImgSrc().None();
builder.AddFormAction().None();
builder.AddFontSrc().None();
builder.AddStyleSrc().None();
builder.AddScriptSrc().None();
builder.AddBaseUri().Self();
builder.AddFrameAncestors().None();
builder.AddCustomDirective("require-trusted-types-for", "'script'");
});
if (!isDev)
{
// maxage = one year in seconds
policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 60 * 60 * 24 * 365);
}
return policy;
}
}
The API calls would return the following headers in the HTTP response. The CSP blocks Javascript.

The Swagger definition allows unsafe Javascript. This allows for XSS attacks and is a weak level of security. This is required due to the way the Swagger UI is created.
public static class SecurityHeadersDefinitionsSwagger
{
private static HeaderPolicyCollection? policy;
public static HeaderPolicyCollection GetHeaderPolicyCollection(bool isDev)
{
// Avoid building a new HeaderPolicyCollection on every request for performance reasons.
// Where possible, cache and reuse HeaderPolicyCollection instances.
if (policy != null) return policy;
policy = new HeaderPolicyCollection()
.AddFrameOptionsDeny()
.AddContentTypeOptionsNoSniff()
.AddReferrerPolicyStrictOriginWhenCrossOrigin()
.AddCrossOriginOpenerPolicy(builder => builder.SameOrigin())
.AddCrossOriginEmbedderPolicy(builder => builder.RequireCorp())
.AddCrossOriginResourcePolicy(builder => builder.SameOrigin())
.RemoveServerHeader()
.AddPermissionsPolicyWithDefaultSecureDirectives();
policy.AddContentSecurityPolicy(builder =>
{
builder.AddObjectSrc().None();
builder.AddBlockAllMixedContent();
builder.AddImgSrc().Self().From("data:");
builder.AddFormAction().Self();
builder.AddFontSrc().Self();
builder.AddStyleSrc().Self().UnsafeInline();
builder.AddScriptSrc().Self().UnsafeInline(); //.WithNonce();
builder.AddBaseUri().Self();
builder.AddFrameAncestors().None();
});
if (!isDev)
{
// maxage = one year in seconds
policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 60 * 60 * 24 * 365);
}
return policy;
}
}
The HTTP response would look something like this:

The security headers are setup to always use the API swagger definitions except for the swagger routes. This is used in development. I still don’t deploy this to production, unless the Swagger UI is absolutely required. In production, all responses use the stricter security header definitions.
// Open up security restrictions to allow this to work
// Not recommended in production
var deploySwaggerUI = builder.Configuration.GetValue<bool>("DeploySwaggerUI");
var isDev = builder.Environment.IsDevelopment();
builder.Services.AddSecurityHeaderPolicies()
.SetPolicySelector((PolicySelectorContext ctx) =>
{
// sum is weak security headers due to Swagger UI deployment
// should only use in development
if (deploySwaggerUI)
{
// Weakened security headers for Swagger UI
if (ctx.HttpContext.Request.Path.StartsWithSegments("/swagger"))
{
return SecurityHeadersDefinitionsSwagger.GetHeaderPolicyCollection(isDev);
}
// Strict security headers
return SecurityHeadersDefinitionsAPI.GetHeaderPolicyCollection(isDev);
}
// Strict security headers for production
else
{
return SecurityHeadersDefinitionsAPI.GetHeaderPolicyCollection(isDev);
}
});
The security headers are added as middleware using the UseSecurityHeaders methods.
app.UseSecurityHeaders();
Notes
This setup works good and the correct headers for the API are used in both development with the Swagger UI or without the Swagger UI. No weaken headers are deployed to production.
Links
https://csp-evaluator.withgoogle.com/
Security by Default Chrome developers
A Simple Guide to COOP, COEP, CORP, and CORS
https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders