Content Security Policy (CSP) is a browser security mechanism that controls which resources a page can load. It's your last line of defence against cross-site scripting (XSS): even if an attacker injects a script tag, CSP can prevent the browser from executing it. Despite this, many ASP.NET Core applications ship without any CSP headers at all.
What CSP Does
CSP works by specifying allowed sources for different resource types. The browser enforces these rules. If a script, stylesheet, image, or font doesn't match the policy, the browser blocks it and logs a violation.
Adding CSP via Middleware
ASP.NET Core doesn't have built-in CSP middleware, but adding it is straightforward:
app.Use(async (context, next) =>
{
context.Response.Headers.Append(
"Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'");
await next();
});
This policy restricts everything to same-origin, blocks framing (clickjacking protection), and restricts form targets.
A More Practical CSP Builder
Hardcoding CSP strings is error-prone. A builder pattern makes policies composable and readable:
public class CspBuilder
{
private readonly Dictionary<string, List<string>> _directives = new();
public CspBuilder AddDirective(string directive, params string[] sources)
{
if (!_directives.ContainsKey(directive))
_directives[directive] = new List<string>();
_directives[directive].AddRange(sources);
return this;
}
public CspBuilder DefaultSrc(params string[] sources) =>
AddDirective("default-src", sources);
public CspBuilder ScriptSrc(params string[] sources) =>
AddDirective("script-src", sources);
public CspBuilder StyleSrc(params string[] sources) =>
AddDirective("style-src", sources);
public CspBuilder ImgSrc(params string[] sources) =>
AddDirective("img-src", sources);
public CspBuilder ConnectSrc(params string[] sources) =>
AddDirective("connect-src", sources);
public CspBuilder FrameAncestors(params string[] sources) =>
AddDirective("frame-ancestors", sources);
public string Build()
{
return string.Join("; ", _directives.Select(
d => $"{d.Key} {string.Join(" ", d.Value)}"));
}
}
Usage:
var csp = new CspBuilder()
.DefaultSrc("'self'")
.ScriptSrc("'self'", "https://cdn.example.com")
.StyleSrc("'self'", "'unsafe-inline'")
.ImgSrc("'self'", "data:", "https://images.example.com")
.ConnectSrc("'self'", "https://api.example.com")
.FrameAncestors("'none'")
.Build();
app.Use(async (context, next) =>
{
context.Response.Headers.Append("Content-Security-Policy", csp);
await next();
});
Nonces for Inline Scripts
Blocking all inline scripts with script-src 'self' is the safest option, but some applications genuinely need inline scripts. Rather than using 'unsafe-inline' (which defeats the purpose of CSP entirely), use nonces:
app.Use(async (context, next) =>
{
var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
context.Items["CspNonce"] = nonce;
context.Response.Headers.Append(
"Content-Security-Policy",
$"default-src 'self'; script-src 'self' 'nonce-{nonce}'");
await next();
});
In your Razor views, apply the nonce:
@{
var nonce = Context.Items["CspNonce"]?.ToString();
}
<script nonce="@nonce">
// This script is allowed because it has the correct nonce
console.log("Hello from an inline script");
</script>
Each request gets a fresh nonce. An attacker who injects a script tag won't know the nonce, so the browser blocks their script.
Report-Only Mode
Rolling out CSP on an existing application without breaking things requires a cautious approach. Use Content-Security-Policy-Report-Only to monitor violations without enforcing the policy:
app.Use(async (context, next) =>
{
context.Response.Headers.Append(
"Content-Security-Policy-Report-Only",
"default-src 'self'; " +
"report-uri /api/csp-report");
await next();
});
app.MapPost("/api/csp-report", async (HttpContext context) =>
{
using var reader = new StreamReader(context.Request.Body);
var report = await reader.ReadToEndAsync();
// Log the violation report
Log.Warning("CSP violation: {Report}", report);
return Results.Ok();
});
Run in report-only mode for a few weeks, analyse the violations, adjust your policy, then switch to enforcement.
Key Directives to Know
default-src: Fallback for all resource types not explicitly configured.script-src: Controls JavaScript sources. The most important directive for XSS prevention.style-src: Controls CSS sources.connect-src: Controls fetch, XHR, and WebSocket connections.frame-ancestors: Controls who can embed your page. ReplacesX-Frame-Options.base-uri: Restricts the<base>tag, preventing attackers from changing the base URL for relative links.form-action: Controls where forms can submit to.
Wrapping Up
CSP is one of the most effective defences against XSS, yet it remains underused. Start with report-only mode on existing applications, use nonces instead of 'unsafe-inline', and build your policy incrementally. A strict CSP won't prevent all attacks, but it dramatically reduces the impact of the ones that get through.