Browsers only enforce the security policy you send. On a production ASP.NET Core site behind IIS, a missing HSTS or frame policy is not theoretical—it is the difference between a forced HTTPS session and a user who can be framed or MIME-sniffed into executing a hostile payload.
You do not need a WAF product to fix the basics. A short middleware block (or a few web.config customHeaders) plus hardened auth cookie flags covers the headers auditors and modern browsers expect. Deploy once, verify with curl, and leave them on for every environment that serves real traffic.
#Headers worth shipping on every response
Keep the set small and intentional. Over-broad CSP breaks admin UIs; under-specified CSP is theater. For most hosted ASP.NET apps these six are the practical baseline:
- Strict-Transport-Security — force HTTPS after the first secure hit (includeSubDomains only if every subdomain is ready).
- X-Content-Type-Options: nosniff — stop MIME sniffing on scripts and styles.
- Content-Security-Policy — start with default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' and tighten from reports.
- Referrer-Policy: strict-origin-when-cross-origin — limit leaking path/query to third parties.
- Permissions-Policy — disable camera, mic, geolocation, payment unless the app needs them.
- Cross-Origin-Opener-Policy: same-origin — reduce cross-window attacks when you do not need popup integrations.
#Add headers in ASP.NET Core 10 middleware
Prefer app-level headers so the same package ships to every IIS site and slot. Register early in the pipeline so static files and error pages inherit them. Example for .NET 10 / ASP.NET Core 10:
app.Use(async (ctx, next) =>
{
var h = ctx.Response.Headers;
h["X-Content-Type-Options"] = "nosniff";
h["Referrer-Policy"] = "strict-origin-when-cross-origin";
h["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()";
h["Cross-Origin-Opener-Policy"] = "same-origin";
h["Content-Security-Policy"] =
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'";
if (ctx.Request.IsHttps)
{
h["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains";
}
await next();
});
Put this before UseStaticFiles and endpoint routing. If you already terminate TLS at a load balancer and IIS only sees HTTP, either enable forwarded headers correctly or emit HSTS only when you know the public URL is HTTPS—never advertise HSTS on a plain-HTTP site you still need to reach.
#web.config fallback for mixed or static sites
Classic ASP.NET, static marketing roots, or apps you cannot rebuild yet can set the same values under system.webServer. On shared Windows hosts this is often the fastest path when you only have Web Deploy access to the site root:
<system.webServer>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
<add name="Permissions-Policy" value="camera=(), microphone=(), geolocation=()" />
<add name="Content-Security-Policy"
value="default-src 'self'; object-src 'none'; frame-ancestors 'none'" />
<add name="Strict-Transport-Security" value="max-age=31536000" />
</customHeaders>
</httpProtocol>
</system.webServer>
Remove X-Powered-By while you are there—it does not improve security much, but it stops advertising the stack. If URL Rewrite already issues HTTPS redirects, keep HSTS max-age modest until you confirm every hostname on the certificate is HTTPS-only.
#Lock down auth and session cookies
Headers without cookie flags leave session tokens readable to script or sent on cross-site requests. For cookie auth in ASP.NET Core 10:
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "__Host-app";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax; // Strict if you have no cross-site POSTs
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
});
The __Host- prefix requires Secure, path=/, and no Domain attribute—good defaults on a single-site host binding. For antiforgery and correlation cookies, set the same HttpOnly/Secure/SameSite values explicitly so a library default does not undo your policy. On IIS, confirm the site binding is HTTPS (SNI cert present) or Secure cookies will never round-trip.
#Verify on the hosted site
After Web Deploy, hit the public URL—not localhost—so you see the same TLS and header path users hit:
curl -sI https://your-app.example | findstr /I "strict-transport content-security x-content referrer permissions set-cookie"
In DevTools, confirm Set-Cookie shows HttpOnly; Secure; SameSite. Then deliberately break CSP in a staging slot (inline script without a nonce) and watch the console—if nothing fails, your policy is too loose. Roll forward only after the app’s real scripts, CDNs, and API origins are listed.
Practical takeaway: ship nosniff, a tight CSP with frame-ancestors 'none', HSTS on HTTPS-only hosts, and __Host- auth cookies with HttpOnly + Secure + SameSite in one change set. Verify with curl -I against the live IIS binding, keep a staging slot for CSP tuning, and treat header/cookie policy as part of every .NET 10 deploy—not a one-off hardening ticket.
Comments
No comments yet