Content Security Policy for Angular hosted by ASP.NET Core is a nonce-based (or hash-based) CSP that still boots your SPA — not unsafe-inline forever and not a one-line header copied from a blog that breaks Material and charts.
Browser requests /
│
▼
ASP.NET Core / BFF
generate nonce
CSP header with nonce-...
index.html script tags include same nonce
│
▼
Angular boots without unsafe-inline scripts
New to this → stay here. Header baseline → security headers. Same-origin BFF → YARP BFF.
Search intent for content security policy angular asp.net core is how-to production CSP for the SPA host.
Why basic security-headers posts are not enough for SPAs
X-Content-Type-Options and frame-ancestors are necessary and comparatively easy. CSP is where Angular teams regress to:
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval'
That undoes the XSS containment CSP is for. Hosting Angular from ASP.NET Core gives you a perfect place to inject per-request nonces into HTML — something a pure CDN static bucket cannot do without edge workers.
CSP directives that matter for Angular
Minimum production-minded set:
| Directive | Typical Angular + API host value |
|---|---|
default-src | 'self' |
script-src | 'self' 'nonce-...' (plus trusted CDNs if any) |
style-src | 'self' 'nonce-...' or hashes; some CSS-in-JS needs extra care |
img-src | 'self' data: https: (tighten to your CDN) |
font-src | 'self' + font CDN if used |
connect-src | 'self' + API origin + SignalR/WebSocket scheme |
frame-ancestors | 'none' or specific parents |
base-uri | 'self' |
object-src | 'none' |
form-action | 'self' |
connect-src must include every API and WebSocket endpoint the browser calls. Forgetting wss: breaks SignalR and looks like a random network error.
Nonce middleware in ASP.NET Core for index.html
public sealed class CspNonceMiddleware
{
private readonly RequestDelegate _next;
public CspNonceMiddleware(RequestDelegate next) => _next = next;
public async Task Invoke(HttpContext context)
{
var nonceBytes = RandomNumberGenerator.GetBytes(16);
var nonce = Convert.ToBase64String(nonceBytes);
context.Items["CspNonce"] = nonce;
context.Response.OnStarting(() =>
{
// Only set CSP on document responses you control; APIs return JSON without HTML nonces
if (context.Response.ContentType?.StartsWith("text/html") == true
|| context.Request.Path == "/" )
{
var csp =
$"default-src 'self'; " +
$"script-src 'self' 'nonce-{nonce}'; " +
$"style-src 'self' 'nonce-{nonce}'; " +
$"img-src 'self' data: https:; " +
$"font-src 'self' data:; " +
$"connect-src 'self' https://api.example.com wss://api.example.com; " +
$"object-src 'none'; base-uri 'self'; frame-ancestors 'none'";
context.Response.Headers.ContentSecurityPolicy = csp;
}
return Task.CompletedTask;
});
await _next(context);
}
}
Serving index.html with nonce replacement:
app.MapGet("/", async (HttpContext ctx) =>
{
var nonce = (string)ctx.Items["CspNonce"]!;
var html = await File.ReadAllTextAsync("wwwroot/index.html");
html = html.Replace("{{CSP_NONCE}}", nonce, StringComparison.Ordinal);
ctx.Response.ContentType = "text/html; charset=utf-8";
await ctx.Response.WriteAsync(html);
});
index.html template:
<script src="main-XXXX.js" nonce="{{CSP_NONCE}}"></script>
Angular production builders emit external .js files — good. Your remaining problem is usually inline scripts (environment injection, third-party snippets) and inline styles.
For static hashed bundles on 'self', nonces on <script src> are optional in modern CSP (external scripts on allowed origins load), but nonces remain critical for any inline bootstrapping.
Alternative: hash-based CSP
If HTML is fully static on a CDN, compute sha256-... hashes for known inline scripts at build time. Nonces fit ASP.NET Core-hosted index better; hashes fit immutable static hosting.
Styles, Material/charts, and common breaks
Angular Material, some chart libraries, and CSS-in-JS may:
- Inject
<style>tags without nonces - Use inline
style=""attributes - Evaluate expressions (
unsafe-eval) — especially older chart code
Mitigations:
- Prefer build pipelines that emit external CSS
- Use
'nonce-...'on style tags if you control SSR/index - As a temporary measure,
style-src 'self' 'unsafe-inline'while you eliminate inline scripts first — scripts are the higher-value XSS vector - Replace libraries that require
unsafe-evalfor admin screens that handle PHI
Google Tag Manager and random marketing pixels are frequent CSP breakers on public sites — isolate marketing to the prerendered marketing host when possible (SSR hosting layout).
Trusted Types bridge overview
Trusted Types (require-trusted-types-for 'script') harden DOM XSS sinks. Angular’s path here evolves by version — treat Trusted Types as a second phase after nonce CSP is stable. Enable Report-Only Trusted Types, fix sinks, then enforce.
Do not enable Trusted Types enforcement the same day you first turn on CSP.
Report-Only rollout strategy
- Ship
Content-Security-Policy-Report-Onlywith the desired policy - Collect reports to an endpoint you own (or a vendor)
- Fix violations for 1–2 sprints
- Switch to enforcing
Content-Security-Policy - Keep reporting
context.Response.Headers["Content-Security-Policy-Report-Only"] = csp + "; report-uri /csp-report";
Prefer report-to/Reporting-Endpoints on modern stacks when you control the reporting infrastructure.
Do not ignore reports from old cached index.html during deploy — short-cache the HTML document (App Service hosting).
Debugging blocked scripts without disabling CSP
Chrome DevTools → Console shows CSP violation details (blocked URI, directive).
Workflow:
- Reproduce with enforcing policy on a staging slot
- Note directive (
script-src,connect-src) - Fix origin allow-list or remove inline
- Never “temporarily” add
unsafe-inlineon production PHI apps to silence errors
curl -I checks header presence; it does not validate Angular boot — use a real browser.
SSR/hydration interactions
If Node SSR emits HTML, it must use the same nonce the CSP header declares. Coordinate proxy and SSR process. Mismatched nonces → blank app after SSR.
Checklist for go-live
- Report-Only in staging for a full week of QA
connect-srcincludes API +wss:- No
unsafe-evalunless exception documented - Inline script inventory eliminated or nonced
- Marketing third-parties scoped or removed from app host
- CSP on HTML responses only (do not break raw API JSON clients)
- Document break-glass owners
- Pair with the rest of security headers
Common mistakes I still see
- CSP only on
/but not on deep-link fallbackindex.html unsafe-inlineleft after “we’ll fix later”- Forgetting WebSocket hosts in
connect-src - Enforcing Trusted Types on day one
- CDN index.html immutable forever while CSP nonces change
Verification
- View-source shows matching nonce on boot scripts and CSP header
- Console clean on login + main dashboard under enforcing policy
- SignalR connects
- Deliberate inline
<script>alert(1)</script>in a test page blocked - Report-Only endpoint receives intentional violation from a test route
If an interviewer asks
How do you CSP an Angular app on ASP.NET Core?
Strong answer: Per-request nonce from the host, inject into CSP and HTML, avoid unsafe-inline for scripts, widen style carefully, Report-Only first, include connect-src for API and websockets. Security-headers baseline is not enough alone for SPAs.
Related: Security headers · BFF YARP · App Service · Standalone Angular
Deep-link fallback must keep CSP
When using MapFallbackToFile, every SPA route still returns index.html. Ensure middleware runs so CSP + nonce apply to fallback responses, not only /.
app.UseMiddleware<CspNonceMiddleware>();
// ...
app.MapFallback("/{**path}", async ctx => { /* read index, replace nonce, write */ });
Static file middleware for *.js should not strip needed headers; JS files typically do not need the HTML CSP nonce header to match, but document responses do.
Separating API CORS from CSP
CORS is not CSP. CORS governs who may call your API from a browser origin. CSP governs what your pages may load/execute. Fix both; do not confuse them when debugging Angular (CORS guide).
Environment bootstrap without inline scripts
Instead of:
<script>window.__env = { apiUrl: '...' }</script>
Prefer:
- Same-origin relative URLs
assets/config.jsonfetched at startup (allowed byconnect-src 'self')- Build-time file replacements per environment
Inline env scripts force nonces or hashes on every deploy.
Third-party scripts policy
Allow-list exact hosts (https://js.stripe.com) rather than https:. Review quarterly. Marketing tags belong on the marketing site, not the authenticated clinician shell.
CSP and file uploads / blob URLs
If the UI previews images via blob: URLs, include blob: in img-src (and sometimes media-src). Tighten when the feature ships without previews.
Rollout timeline example
| Day | Action |
|---|---|
| 1 | Report-Only policy deployed |
| 3 | Fix top violations (charts, fonts) |
| 10 | Re-test Material screens |
| 14 | Enforce on staging |
| 21 | Enforce on production with reporting still on |
Extra verification
- Lighthouse does not require CSP, but DevTools Security panel should show CSP present
- Automated browser test asserts
documentCSP via meta or headers - Attempted XSS payload in a rich text field does not execute script
Example final CSP string (illustrative)
default-src 'self';
script-src 'self' 'nonce-RAND';
style-src 'self' 'nonce-RAND';
img-src 'self' data: https://cdn.example.com;
font-src 'self';
connect-src 'self' https://api.example.com wss://api.example.com;
frame-ancestors 'none';
base-uri 'self';
object-src 'none';
form-action 'self'
Replace CDNs with your real hosts. Remove https://cdn.example.com if unused.
Angular builder notes
With application builder / esbuild pipelines, prefer configurations that avoid inline runtime. If a plugin injects inline scripts, fix the plugin or hash that exact inline in CI as a build step.
Incident response
If a production CSP block stops login:
- Do not disable CSP globally as first reaction
- Use Report-Only on the slot or widen a single directive with a ticket
- Fix root cause (new third-party, missing connect-src)
- Re-enforce
Document this break-glass in the runbook next to App Service deploy notes.
Meta tag vs header
Prefer HTTP headers for CSP from ASP.NET Core. <meta http-equiv="Content-Security-Policy"> cannot fully replace framing protections and is easier to miss on some responses. If both exist, keep them consistent.
Service worker considerations
If you use Angular service workers, review cache strategies so an old index.html without the right nonce strategy does not linger. Prefer network-first for the document.
Nonce lifetime and CDN caching of HTML
Never cache index.html for long at the CDN if it embeds a nonce that must match a per-request CSP header. Either:
- Bypass CDN for the document and cache only hashed JS/CSS, or
- Use hash-based CSP for fully static HTML without per-request nonces
ASP.NET Core-hosted documents with nonces should send Cache-Control: no-store (or very short revalidation) for HTML.
Pairing with BFF
When YARP/BFF serves the SPA (BFF pattern), put CSP middleware on the BFF host that returns HTML — not only on the API project that no longer serves wwwroot.