Customize security headers per request
mainHeaderPolicyCollection immediately before it is applied. This enables per-request customization of the headers being sent to the client.repository·main·Indexed 21 days ago
https://github.com/andrewlock/netescapades.aspnetcore.securityheadersA lightweight ASP.NET Core package to simplify the addition and customization of security-related HTTP response headers. It provides middleware for applying default security headers, support for Content-Security-Policy (CSP) and Permissions-Policy, and the ability to define named policies for specific endpoints. Optional TagHelpers are available for implementing nonces and auto-generated SHA256 hashes in Razor views.
HeaderPolicyCollection immediately before it is applied. This enables per-request customization of the headers being sent to the client.To customize headers, instantiate a HeaderPolicyCollection and use its helper methods to add or modify policies. You can also add arbitrary headers using AddCustomHeader.
Common patterns include:
AddDefaultSecurityHeaders() as a base and then overriding specific headers.UseSecurityHeaders(Action<HeaderPolicyCollection> ...) overload for a more concise syntax.RemoveServerHeader() to strip the Server header.public void Configure(IApplicationBuilder app)
{
// Option 1: Explicitly creating the collection
var policyCollection = new HeaderPolicyCollection()
.AddDefaultSecurityHeaders()
.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 63072000);
app.UseSecurityHeaders(policyCollection);
// Option 2: Using the Action overload (more terse)
app.UseSecurityHeaders(policies =>
policies
.AddDefaultSecurityHeaders()
.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 63072000)
);
}The RemoveServerHeader() method in the HeaderPolicyCollection may not be sufficient to remove the Server header because Kestrel often adds it late in the middleware pipeline.
To reliably prevent Kestrel from adding the Server header, you should configure KestrelServerOptions in your Program.cs when constructing your WebHostBuilder.
var host = new WebHostBuilder()
.UseKestrel(options => options.AddServerHeader = false)
//...To apply a set of safe, minimum default security headers to all responses, add the UseSecurityHeaders() middleware to your application pipeline.
Important: The order of middleware matters. To ensure headers are applied to all requests, configure this middleware as early as possible in your Startup pipeline or WebApplication configuration.
public void Configure(IApplicationBuilder app)
{
app.UseSecurityHeaders();
// other middleware e.g. static files, MVC etc
}If your API returns only JSON, you may not need the full set of security headers provided by AddDefaultSecurityHeaders(). You can use AddDefaultApiSecurityHeaders() to apply a more focused subset of headers recommended by OWASP.
This method sets:
X-Content-Type-Options: nosniffStrict-Transport-Security: max-age=31536000; (HTTPS only)X-Frame-Options: DenyContent-Security-Policy: default-src: none; frame-ancestors 'none'Referrer-Policy: no-referrerPermissions-Policy: A comprehensive list of disabled features (e.g., camera=(), geolocation=(), etc.)Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corpCross-Origin-Resource-Policy: same-sitepublic void Configure(IApplicationBuilder app)
{
var policyCollection = new HeaderPolicyCollection()
.AddDefaultApiSecurityHeaders();
app.UseSecurityHeaders(policyCollection);
}To verify the provenance of NetEscapades NuGet packages, you must first remove the .signature.p7s file added by nuget.org to reconstruct the original package.
1. Remove the signature file (Linux/macOS):
file="path/to/NetEscapades.AspNetCore.SecurityHeaders.1.3.1.nupkg"
zip -d $file .signature.p7s2. Remove the signature file (PowerShell):
$file="path/to/NetEscapades.AspNetCore.SecurityHeaders.1.3.1.nupkg"
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression')
$stream = New-Object IO.FileStream($file, [IO.FileMode]::Open)
$zip = New-Object IO.Compression.ZipArchive($stream, [IO.Compression.ZipArchiveMode]::Update)
$zip.Entries | ? { $_.Name -eq ".signature.p7s" } | % { $_.Delete() }
$zip.Dispose();3. Verify with GitHub CLI:
gh attestation verify --owner andrewlock "NetEscapades.AspNetCore.SecurityHeaders.1.3.1.nupkg"4. Verify SBOM attestations:
gh attestation verify --owner andrewlock --predicate-type https://cyclonedx.org/bom "NetEscapades.AspNetCore.SecurityHeaders.1.3.1.nupkg"To use nonces and auto-generated hashes with Content-Security-Policy (CSP) in Razor views, you must install the NetEscapades.AspNetCore.SecurityHeaders.TagHelpers NuGet package in addition to the core library.
dotnet package add Install-Package NetEscapades.AspNetCore.SecurityHeaders.TagHelpersTo allow-list inline content using SHA256 hashes, use the asp-add-content-to-csp attribute on <script> or <style> tags.
<script> and <style> tags, the attribute automatically calculates the hash of the element's content and adds it to the CSP header.csp-hash-type attribute (options: SHA256, SHA384, SHA512).<script asp-add-content-to-csp>
var msg = document.getElementById('message');
msg.innerText = "I'm allowed";
</script>
<style asp-add-content-to-csp csp-hash-type="SHA384">
#message {
color: @color;
}
</style>Since inline styles and event handlers do not support nonces, use the AttributeHashTagHelper. Apply the asp-add-csp-for-* attribute to an element, where * is the name of the attribute you want to hash (e.g., style or onclick).
asp-add-csp-for-style for style attributes.asp-add-csp-for-onclick for onclick attributes.asp-add-csp-for-onclick="SHA384").<h3 asp-add-csp-for-style style="color: red">I will be styled red</h3>
<button asp-add-csp-for-style style="color: red" asp-add-csp-for-onclick="SHA384" onclick="alert('Hello!')">Click me!</button>For scenarios like multi-tenancy where headers must change based on request data (e.g., a tenant ID in a header), use SetPolicySelector() on IServiceCollection.AddSecurityHeaderPolicies().
This method accepts a Func<PolicySelectorContext, IReadOnlyHeaderPolicyCollection>. The selector is invoked for every request.
Best Practice: Avoid creating a new HeaderPolicyCollection from scratch on every request. Instead, cache policies or use IReadOnlyHeaderPolicyCollection.Copy() to create a mutable copy of an existing policy to modify.
builder.Services.AddSecurityHeaderPolicies()
.AddPolicy("TenantPolicy", p => p.AddCustomHeader("X-Tenant", "Default"))
.SetPolicySelector((PolicySelectorContext ctx) =>
{
// Access DI services via HttpContext
var services = ctx.HttpContext.RequestServices;
var selector = services.GetRequiredService<TenantHeaderPolicyCollectionSelector>();
var tenant = services.GetRequiredService<ITenant>();
return selector.GetPolicy(tenant);
});To use a per-request nonce, follow these steps:
_ViewImports.cshtml:
@addTagHelper *, NetEscapades.AspNetCore.SecurityHeaders.TagHelpersasp-add-nonce attribute on <script> or <style> tags. The TagHelper will automatically attach the required nonce="..." attribute to the element at runtime.<script asp-add-nonce>
var body = document.getElementsByTagName('body')[0];
var div = document.createElement('div');
div.innerText = "I was added using the NonceHelper";
body.appendChild(div);
</script>You can install the package via the Visual Studio Package Manager Console or the dotnet CLI.
Package Manager Console:
PM> Install-Package NetEscapades.AspNetCore.SecurityHeadersdotnet CLI:
dotnet add package NetEscapades.AspNetCore.SecurityHeaders --version 1.3.1