Content Security Policy
A Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of scripts, styles, images, and other resources it is allowed to load and execute. It is your single most effective defense-in-depth control against cross-site scripting (XSS): even if an attacker manages to inject markup, a strict CSP prevents the injected script from running. Angular has first-class CSP support — including automatic nonce propagation for its inline styles and a native integration with the browser’s Trusted Types API — but you must configure the header yourself and feed Angular a nonce so its runtime styles aren’t blocked.
How CSP works
The browser reads the Content-Security-Policy header and enforces each directive. Anything not explicitly allowed is refused, and a violation is logged to the console (and optionally reported to an endpoint). A modern, strict policy avoids the brittle URL allow-lists of the past and instead trusts scripts by a per-response cryptographic nonce or hash.
| Directive | Controls | Typical strict value |
|---|---|---|
default-src | Fallback for unlisted resource types | 'self' |
script-src | JavaScript execution | 'nonce-<random>' 'strict-dynamic' |
style-src | Stylesheets and inline styles | 'self' 'nonce-<random>' |
object-src | <object>, <embed> (legacy plugin vectors) | 'none' |
base-uri | The <base href> element | 'self' |
require-trusted-types-for | Forces Trusted Types on DOM sinks | 'script' |
A nonce (“number used once”) must be a fresh, unguessable, base64 value generated on the server for every response. Reusing a nonce across requests defeats the protection entirely.
Generating and applying the header
CSP lives at the server or edge, not in Angular’s static index.html, because the nonce changes per request. Below is an Express handler (the same pattern applies to a Node SSR server or a CDN edge function) that mints a nonce, injects it into the served HTML, and emits the matching header.
import express from 'express';
import { randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
const app = express();
const template = readFileSync('dist/app/browser/index.html', 'utf-8');
app.get('*', (req, res) => {
const nonce = randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
[
`default-src 'self'`,
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`object-src 'none'`,
`base-uri 'self'`,
`require-trusted-types-for 'script'`,
].join('; '),
);
// Angular reads the nonce from the ngCspNonce attribute on its root element.
const html = template.replace('<app-root', `<app-root ngCspNonce="${nonce}"`);
res.send(html);
});
app.listen(4000);
Output:
$ curl -sI http://localhost:4000/ | grep -i content-security
content-security-policy: default-src 'self'; script-src 'nonce-9Qm0...'
'strict-dynamic'; style-src 'self' 'nonce-9Qm0...'; object-src 'none';
base-uri 'self'; require-trusted-types-for 'script'
Nonce-based styles in Angular
Angular’s runtime injects <style> elements for component styles and animations. Under a strict style-src, those inline styles would be blocked. The fix is the ngCspNonce attribute on the application root element (as shown above). Angular reads that nonce at bootstrap and stamps it onto every style tag it creates, so they pass the policy.
If you bootstrap programmatically rather than through markup, supply the nonce via CSP_NONCE instead:
import { bootstrapApplication } from '@angular/platform-browser';
import { CSP_NONCE } from '@angular/core';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
{ provide: CSP_NONCE, useValue: (window as any).__nonce__ },
],
});
Provide the nonce through exactly one mechanism. If both ngCspNonce and CSP_NONCE are present, the ngCspNonce attribute wins.
Integrating Trusted Types
Trusted Types is a browser API that locks down DOM injection sinks like innerHTML and script.src, requiring values to pass through a registered policy before assignment. Combined with CSP it eliminates the most dangerous DOM-XSS vectors. Enable it with the require-trusted-types-for 'script' directive plus a trusted-types directive naming the policies you allow.
Angular ships a built-in policy named angular (used by its sanitizer) and angular#bundler for the build output. You almost never call the API directly — Angular’s DomSanitizer already produces compliant values — but you must allow-list Angular’s policies:
res.setHeader(
'Content-Security-Policy',
[
`require-trusted-types-for 'script'`,
`trusted-types angular angular#bundler`,
].join('; '),
);
If a third-party library assigns to innerHTML directly, the browser throws a TypeError. Wrap such code in your own named policy and add the name to the trusted-types list:
const policy = window.trustedTypes!.createPolicy('legacy-widget', {
createHTML: (input: string) => input, // sanitize here in real code
});
element.innerHTML = policy.createHTML(markup) as unknown as string;
Start with
Content-Security-Policy-Report-Onlyand areport-togroup. It surfaces every violation without breaking the app, letting you tune the policy before switching to the enforcing header.
Best practices
- Prefer nonces with
'strict-dynamic'over host allow-lists; allow-lists are easy to bypass and painful to maintain. - Generate a fresh, cryptographically random nonce per response and never cache the HTML that embeds it.
- Always set
object-src 'none'andbase-uri 'self'— these close common bypass paths thatscript-srcalone misses. - Roll out with
Content-Security-Policy-Report-Onlyand a reporting endpoint, then promote to the enforcing header once violations are clean. - Allow-list only
angularandangular#bundlerfor Trusted Types, and register named policies for any unavoidable legacy DOM writes. - Keep the policy in your server or edge layer, not in
index.html, so the nonce can vary per request.