Helmet.AspNetCore
0.1.0
dotnet add package Helmet.AspNetCore --version 0.1.0
NuGet\Install-Package Helmet.AspNetCore -Version 0.1.0
<PackageReference Include="Helmet.AspNetCore" Version="0.1.0" />
<PackageVersion Include="Helmet.AspNetCore" Version="0.1.0" />
<PackageReference Include="Helmet.AspNetCore" />
paket add Helmet.AspNetCore --version 0.1.0
#r "nuget: Helmet.AspNetCore, 0.1.0"
#:package Helmet.AspNetCore@0.1.0
#addin nuget:?package=Helmet.AspNetCore&version=0.1.0
#tool nuget:?package=Helmet.AspNetCore&version=0.1.0
Helmet.AspNetCore
A security headers library for ASP.NET Core that goes beyond NWebsec: content-type-aware header application, full CSP builder with nonce and report-only support, profile-based defaults, per-endpoint overrides, and server hardening — in a single package.
Features
- Content-type-aware — HTML responses get the full security header set (CSP, X-Frame-Options, Permissions-Policy, etc.); API/JSON responses get only what's relevant (nosniff, CORP, Cache-Control). Automatic, zero configuration.
- Profile-based defaults —
SecurityProfile.SpaorSecurityProfile.Apigives you a secure baseline in one line. Override individual headers without losing the rest. - Full CSP builder — Fluent API for all CSP Level 3 directives. Nonce support, hash support (SHA-256/384/512),
strict-dynamic, report-only mode. - Per-endpoint overrides — MVC attributes to suppress headers, swap CSP policies, or tweak individual headers on specific actions.
- Server hardening — Kestrel limits, HSTS, rate limiting, CORS, redirect validation — all from the same builder.
Quick Start
dotnet add package Helmet.AspNetCore
// Program.cs — one line for a secure SPA
var builder = WebApplication.CreateBuilder(args);
builder.AddHelmetWebSecurity(SecurityProfile.Spa);
var app = builder.Build();
app.UseHelmetWebSecurity();
app.MapControllers();
app.Run();
That's it. Your app now has:
- CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy on HTML responses
- Nosniff, CORP, Cache-Control on API responses
- HSTS (1-year, includeSubDomains)
- Kestrel hardened (no Server header, request size limits)
- Rate limiting (200 req/min per IP for SPA, 100 for API)
- CORS deny-all
Profiles
SecurityProfile.Spa
For web applications that serve HTML pages (Angular, React, Razor, Blazor).
CSP: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'; upgrade-insecure-requests; block-all-mixed-content
Headers on HTML responses: CSP, X-Frame-Options (DENY), X-XSS-Protection (0), Referrer-Policy (no-referrer), Permissions-Policy (all features blocked), COOP (same-origin), COEP (require-corp), CORP (same-origin), X-Download-Options (noopen), X-Robots-Tag (none), Cache-Control (no-store).
Headers on non-HTML responses: X-Content-Type-Options (nosniff), CORP (same-origin), X-Permitted-Cross-Domain-Policies (none), Cache-Control (no-store).
Rate limit: 200 requests/minute per IP.
SecurityProfile.Api
For pure API services that never serve HTML.
CSP: default-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content
Rate limit: 100 requests/minute per IP.
Since API responses are never text/html, the HTML-only headers are never applied — the profile's CSP is only emitted if something unexpectedly returns HTML.
Content-Type-Aware Headers
The middleware inspects Response.ContentType (via OnStarting callback) and applies only the relevant headers:
| Response type | Headers applied |
|---|---|
text/html, application/xhtml+xml |
Full set: CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy, COOP, COEP, CORP, X-XSS-Protection, X-Download-Options, X-Robots-Tag, Reporting-Endpoints, Cache-Control, nosniff |
| Everything else (JSON, images, CSS, JS, etc.) | Minimal set: nosniff, CORP, X-Permitted-Cross-Domain-Policies, Cache-Control |
| No content type (204, redirects) | Minimal set |
This is automatic. No path-prefix configuration needed.
CSP Configuration
Override specific directives (keep profile defaults for the rest)
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureCsp(csp => csp
.ScriptSrc(s => s.Self().StrictDynamic())
.ConnectSrc(c => c.Self().Source("https://api.example.com")));
Only script-src and connect-src are overridden. All other Spa profile directives remain.
Add hashes for inline scripts/styles
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.AddScriptHash("sha256-MhtPZXr7+LpJUY5qtMutB+qWfQtMaPccfe7QXtCcEYc=")
.AddStyleHash("sha256-abc123...");
Or via the CSP builder for more control:
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureCsp(csp => csp
.ScriptSrc(s => s
.Self()
.UnsafeHashes()
.Sha256("MhtPZXr7+LpJUY5qtMutB+qWfQtMaPccfe7QXtCcEYc="))
.StyleSrc(s => s
.Self()
.Sha256("abc123...")));
Full custom CSP (no profile)
builder.AddHelmetWebSecurity(b =>
{
b.AddSecurityHeaders(headers =>
{
headers.ContentSecurityPolicy(csp => csp
.DefaultSrc(d => d.None())
.ScriptSrc(s => s.Self().Nonce().StrictDynamic())
.StyleSrc(s => s.Self())
.ImgSrc(i => i.Self().DataScheme())
.FontSrc(f => f.Self())
.ConnectSrc(c => c.Self())
.FrameAncestors(fa => fa.None())
.BaseUri(b => b.Self())
.FormAction(f => f.Self())
.UpgradeInsecureRequests());
});
b.HardenKestrel();
b.AddHsts();
b.AddRateLimiting();
b.AddCors();
});
Available CSP directives
| Method | CSP directive |
|---|---|
DefaultSrc |
default-src |
ScriptSrc |
script-src |
ScriptSrcElem |
script-src-elem |
ScriptSrcAttr |
script-src-attr |
StyleSrc |
style-src |
StyleSrcElem |
style-src-elem |
StyleSrcAttr |
style-src-attr |
ImgSrc |
img-src |
FontSrc |
font-src |
ConnectSrc |
connect-src |
MediaSrc |
media-src |
FrameSrc |
frame-src |
ChildSrc |
child-src |
WorkerSrc |
worker-src |
ObjectSrc |
object-src |
ManifestSrc |
manifest-src |
BaseUri |
base-uri |
FormAction |
form-action |
FrameAncestors |
frame-ancestors |
Sandbox |
sandbox |
UpgradeInsecureRequests |
upgrade-insecure-requests |
BlockAllMixedContent |
block-all-mixed-content |
ReportTo |
report-to |
ReportUri |
report-uri |
Available source values
| Method | CSP value |
|---|---|
None() |
'none' |
Self() |
'self' |
UnsafeInline() |
'unsafe-inline' |
UnsafeEval() |
'unsafe-eval' |
UnsafeHashes() |
'unsafe-hashes' |
StrictDynamic() |
'strict-dynamic' |
Nonce() |
'nonce-<per-request>' |
Sha256(hash) |
'sha256-<hash>' |
Sha384(hash) |
'sha384-<hash>' |
Sha512(hash) |
'sha512-<hash>' |
Source(uri) |
Custom origin (e.g. https://cdn.example.com) |
DataScheme() |
data: |
BlobScheme() |
blob: |
Nonce Support
Nonces provide per-request script/style authorization. Enable them with UseNonces():
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureCsp(csp => csp
.ScriptSrc(s => s.Self().Nonce().StrictDynamic()))
.UseNonces();
A 128-bit cryptographic nonce is generated per request and automatically included in the CSP header.
Accessing the nonce in code
// In a controller or middleware
var nonce = HttpContext.GetCspNonce();
Razor tag helper
Add the tag helper to _ViewImports.cshtml:
@addTagHelper *, Helmet.AspNetCore
Then use csp-nonce on script/style tags:
<script csp-nonce>
console.log("This script is nonce-authorized");
</script>
<style csp-nonce>
body { margin: 0; }
</style>
Renders as:
<script nonce="K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=">
console.log("This script is nonce-authorized");
</script>
Angular / SPA note
For pre-built SPAs, hash-based CSP with SRI (subresourceIntegrity: true in angular.json) is usually a better fit than nonces, since the HTML is static. Use AddScriptHash() / AddStyleHash() for inline event handlers or critical CSS that Angular injects.
CSP Report-Only
Test a stricter CSP without breaking your app. Violations are reported but not blocked.
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureCsp(csp => csp
.ScriptSrc(s => s.Self())) // enforced
.ConfigureCspReportOnly(csp => csp
.ScriptSrc(s => s.Self().StrictDynamic()) // test stricter policy
.ReportTo("csp-violations")) // report violations
.AddReportingEndpoint("csp-violations", "/api/security/csp-report");
This produces two headers:
Content-Security-Policy: ... script-src 'self' ...
Content-Security-Policy-Report-Only: ... script-src 'self' 'strict-dynamic' ...; report-to csp-violations
Reporting-Endpoints: csp-violations="/api/security/csp-report"
Built-in report endpoint
Optionally map a minimal endpoint that logs CSP violation reports:
app.MapCspReportEndpoint("/api/security/csp-report");
Reports are logged via ILogger at Warning level. For production, wire your own endpoint to forward reports to Sentry, Elastic, or a dedicated CSP reporting service.
Individual Header Configuration
Override any header while keeping profile defaults for the rest:
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureFrameOptions(FrameOptions.SameOrigin)
.ConfigureReferrerPolicy(ReferrerPolicy.StrictOriginWhenCrossOrigin)
.ConfigureXRobotsTag(r => r.NoIndex().NoFollow().NoSnippet())
.ConfigurePermissionsPolicy(pp => pp.AllowSelf("fullscreen").AllowSelf("clipboard-read"))
.ConfigureCrossOrigin(co => co
.OpenerPolicy(CrossOriginOpenerPolicy.SameOriginAllowPopups)
.EmbedderPolicy(CrossOriginEmbedderPolicy.Credentialless));
FrameOptions
| Value | Header |
|---|---|
FrameOptions.Deny (default) |
X-Frame-Options: DENY |
FrameOptions.SameOrigin |
X-Frame-Options: SAMEORIGIN |
ReferrerPolicy
| Value | Header |
|---|---|
ReferrerPolicy.NoReferrer (default) |
Referrer-Policy: no-referrer |
ReferrerPolicy.StrictOriginWhenCrossOrigin |
Referrer-Policy: strict-origin-when-cross-origin |
ReferrerPolicy.SameOrigin |
Referrer-Policy: same-origin |
| ... | All standard values supported |
Permissions-Policy
Profile default blocks all ~30 features. Open specific ones:
.ConfigurePermissionsPolicy(pp => pp
.AllowSelf("camera") // camera=(self)
.AllowSelf("fullscreen") // fullscreen=(self)
.AllowAll("autoplay") // autoplay=*
.Allow("geolocation", "https://maps.example.com")) // geolocation=(https://maps.example.com)
Cross-Origin Policies
.ConfigureCrossOrigin(co => co
.OpenerPolicy(CrossOriginOpenerPolicy.SameOriginAllowPopups) // for OAuth popups
.EmbedderPolicy(CrossOriginEmbedderPolicy.Credentialless) // relaxed COEP
.ResourcePolicy(CrossOriginResourcePolicy.SameOrigin)) // default
Per-Endpoint Overrides
Suppress all security headers
[SuppressSecurityHeaders]
[HttpGet("/health")]
public IActionResult Health() => Ok("healthy");
Named CSP policies
Register alternative policies at startup:
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.AddCspPolicy("legacy", csp => csp
.ScriptSrc(s => s.Self().UnsafeInline())
.StyleSrc(s => s.Self().UnsafeInline()));
Apply to specific endpoints:
[CspPolicy("legacy")]
public IActionResult LegacyPage() => View();
Individual header overrides
[FrameOptions(FrameOptions.SameOrigin)] // allow embedding this page
public IActionResult EmbeddableWidget() => View();
[CspScriptSrc("'self'", "'unsafe-inline'")] // relax script-src for this page
public IActionResult AdminPage() => View();
Redirect Validation
Prevent open redirect attacks by validating redirect targets. Opt-in, disabled by default.
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureRedirectValidation(rv => rv
.AllowSameHost()
.AllowHost("auth.example.com")
.AllowHost("*.example.com"));
Inspects 3xx responses. If the Location header points to a host not in the allowlist, logs a warning.
Server Hardening
These features are applied automatically by profiles but can be configured individually.
Kestrel
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.HardenKestrel(k =>
{
k.MaxRequestBodySize = 50 * 1024 * 1024; // 50 MB (default: 30 MB)
k.MaxConcurrentConnections = 200; // default: 100
k.MaxRequestHeadersTotalSize = 64 * 1024; // 64 KB (default: 32 KB)
});
Removes the Server header and applies request size / connection limits.
HSTS
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.AddHsts(hsts =>
{
hsts.Preload = true; // add to browser preload list
hsts.MaxAge = TimeSpan.FromDays(730); // 2 years
});
Default: 1-year max-age, includeSubDomains.
Rate Limiting
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.AddRateLimiting(rl =>
{
rl.PermitLimit = 300; // requests per window
rl.WindowMinutes = 1; // window duration
});
Fixed-window rate limiting per remote IP. Returns 429 Too Many Requests when exceeded.
CORS
// Allow specific origins
builder.AddHelmetWebSecurity(SecurityProfile.Api)
.AddCors(policy => policy
.WithOrigins("https://app.example.com")
.AllowCredentials()
.WithMethods("GET", "POST"));
Default: deny all cross-origin requests.
Complete Examples
Angular SPA with OIDC
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.AddScriptHash("sha256-MhtPZXr7+LpJUY5qtMutB+qWfQtMaPccfe7QXtCcEYc=") // Angular critical CSS onload
.ConfigureCsp(csp => csp
.ConnectSrc(c => c.Self().Source("https://auth.example.com"))) // allow OIDC calls
.ConfigureCrossOrigin(co => co
.OpenerPolicy(CrossOriginOpenerPolicy.SameOriginAllowPopups)); // OIDC popup login
var app = builder.Build();
app.UseHelmetWebSecurity();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapFallbackToFile("/index.html").RequireAuthorization();
app.Run();
Pure REST API
builder.AddHelmetWebSecurity(SecurityProfile.Api)
.AddCors(policy => policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Authorization", "Content-Type"));
var app = builder.Build();
app.UseHelmetWebSecurity();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Razor Pages with nonces
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureCsp(csp => csp
.ScriptSrc(s => s.Self().Nonce().StrictDynamic())
.StyleSrc(s => s.Self().Nonce()))
.UseNonces();
var app = builder.Build();
app.UseHelmetWebSecurity();
// ...
<script csp-nonce src="~/js/site.js"></script>
<style csp-nonce>
/* critical CSS */
</style>
Gradual CSP rollout with Report-Only
// Step 1: Deploy with report-only to discover violations
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureCspReportOnly(csp => csp
.ScriptSrc(s => s.Self().StrictDynamic())
.ReportTo("csp"))
.AddReportingEndpoint("csp", "/api/security/csp-report");
// Step 2: After fixing violations, enforce
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.ConfigureCsp(csp => csp
.ScriptSrc(s => s.Self().StrictDynamic()));
Comparison with NWebsec
| Feature | Helmet.AspNetCore | NWebsec |
|---|---|---|
| Content-type-aware headers | Yes (automatic) | No (applies to all) |
| Profile-based defaults | Yes (Spa/Api) | No (manual setup) |
| CSP builder | Yes (fluent) | Yes (fluent) |
| CSP Report-Only | Yes | Yes |
| CSP nonce (tag helper) | Yes | Yes |
| CSP hash support | Yes (SHA-256/384/512) | Yes |
strict-dynamic |
Yes | Yes |
report-to + Reporting-Endpoints |
Yes (modern) | report-uri only (legacy) |
| Per-endpoint overrides | Yes (attributes) | Yes (attributes) |
| Permissions-Policy | Yes (30+ features) | No |
| Cross-Origin isolation (COOP/COEP/CORP) | Yes | No |
| Kestrel hardening | Yes | No |
| Rate limiting | Yes | No |
| Redirect validation | Yes | Yes |
| Server header removal | Yes (via Kestrel) | No |
| HSTS | Yes | Yes |
| X-Robots-Tag | Yes (configurable) | Yes (configurable) |
Migration
Zero breaking changes for existing consumers:
// These continue to work unchanged:
builder.AddHelmetWebSecurity(SecurityProfile.Spa)
.AddScriptHash("sha256-MhtPZXr7+LpJUY5qtMutB+qWfQtMaPccfe7QXtCcEYc=");
builder.AddHelmetWebSecurity(SecurityProfile.Api);
Removed (no current consumers):
SkipBrowserHeaders()— replaced by automatic content-type detectionCustomCspproperty — replaced byConfigureCsp()
Implementation Phases
| Phase | Scope | Delivers |
|---|---|---|
| 1 | Content-type-aware middleware + configurable headers + CSP builder | Fixes original question; makes every header configurable; full CSP directive control |
| 2 | Nonce + report-only + reporting endpoints + sandbox builder | Modern CSP best practices; safe rollout via report-only |
| 3 | Per-endpoint attributes + redirect validation + tag helper | NWebsec feature parity and beyond |
NuGet Packaging
The .csproj must include the README for NuGet:
<PropertyGroup>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
Testing
- Unit tests: CspBuilder/CspSourceBuilder string output, content-type classification (
IsHtmlResponse), policy resolution from profile + overrides, nonce format/uniqueness, redirect validation logic, PermissionsPolicyBuilder output, XRobotsTagBuilder output - Integration tests: Full middleware pipeline with various content types verifying correct header subsets; per-endpoint attribute overrides; nonce present in both CSP header and HttpContext; report-only header generation
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 146 | 4/22/2026 |