Summary
Content Security Policy (CSP) tells the browser which scripts are allowed to run, blocking injected code even when server-side sanitization misses something. Trusted Types closes the remaining DOM XSS surface by requiring that dangerous sinks — innerHTML, eval, script.src — only accept policy-wrapped values, not raw strings.
Jump to the interview angleContent Security Policy + Trusted Types
CSP is an HTTP response header that restricts which scripts, styles, and resources the browser will load. A nonce-based strict policy avoids the fragility of URL allowlists entirely: every inline script that should run carries a cryptographically random nonce the server generates per response.
Trusted Types is a complementary browser API that closes DOM XSS — attacks where attacker-controlled strings flow into sinks like innerHTML. With require-trusted-types-for 'script', the browser rejects raw strings at those sinks and only accepts objects produced by a named TrustedTypePolicy.
Why URL allowlists fail
- Any allowlisted origin (CDN, analytics) can host attacker-uploaded scripts.
- Wildcard domains like *.example.com let sub-domain takeover bypass the policy.
- `unsafe-inline` negates injection protection entirely — it's the default fallback.
- Nonce + strict-dynamic avoids the allowlist problem: no URLs needed.
- `base-uri 'none'` and `object-src 'none'` close two bypass vectors often missed.
Nonce CSP header + Trusted Types policy
The server mints a nonce per request and stamps it on every legitimate inline script. The browser rejects anything else.
// middleware.ts (Next.js 15+)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { randomBytes } from "node:crypto";
export function middleware(req: NextRequest) {
const nonce = randomBytes(16).toString("base64");
const csp = [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'none'",
`require-trusted-types-for 'script'`,
`trusted-types default dompurify`,
"report-uri /csp-report",
].join("; ");
const res = NextResponse.next();
res.headers.set("Content-Security-Policy", csp);
res.headers.set("x-nonce", nonce); // pass to layout via header
return res;
}strict-dynamic propagates the nonce trust to dynamically inserted scripts, so bundlers work without listing every chunk URL.
Trusted Types policy for safe HTML injection
Any code that needs to set innerHTML must go through a named policy. The browser throws a TypeError if raw strings reach the sink.
import DOMPurify from "dompurify";
// Create a named policy — 'dompurify' must match the trusted-types directive.
const policy = window.trustedTypes!.createPolicy("dompurify", {
createHTML: (dirty: string) => DOMPurify.sanitize(dirty),
});
// Safe: goes through the policy
el.innerHTML = policy.createHTML(userContent);
// Unsafe: browser throws TypeError — no raw strings accepted
// el.innerHTML = userContent; ← blockedThe policy name must appear in the trusted-types CSP directive. Any attempt to bypass it throws synchronously before the DOM is mutated.
Roll out with report-only first
Set Content-Security-Policy-Report-Only alongside your existing headers before enforcing. Point report-uri at a collector and run for at least one week. Violations reveal inline scripts and sinks you missed; fix them before switching to the enforcing Content-Security-Policy header. Trusted Types violations appear in the same report stream under trusted-types-sink and trusted-types-policy directives.
Tradeoffs
Pros
- Blocks injected scripts even when input sanitization has a gap.
- Nonces eliminate fragile URL allowlists and CDN-hosted bypass risk.
- Trusted Types catches DOM XSS at the sink — the last line of defense.
- Report-only mode enables incremental rollout with zero user impact.
Cons
- Nonce generation requires server-side per-request work; static CDN serving needs a reverse proxy.
- Trusted Types requires migrating every innerHTML, eval, and script.src call — substantial in large codebases.
- Browser support isn't universal — verify on Baseline/caniuse and treat Trusted Types as defense-in-depth, not a sole backstop.
- A misconfigured policy that allows too-broad sanitization gives false confidence.
Interview angle
Interviewers ask why allowlists break and how nonces + strict-dynamic fix them, then probe Trusted Types as a DOM XSS backstop. Show you know report-only rollout. Soundbite: "Nonce + strict-dynamic kills injection; Trusted Types kills DOM XSS sinks."
Key terms
- CSP nonce
- A per-response random token that allowlists exactly the inline scripts tagged with it.
- strict-dynamic
- CSP keyword that propagates nonce trust to scripts loaded dynamically by an already-trusted script.
- Trusted Types
- Browser API that enforces type-safe values on dangerous DOM sinks (innerHTML, eval, script.src).
- DOM XSS sink
- A DOM API that executes or injects HTML/JS: innerHTML, eval, document.write, script.src.
- report-only mode
- CSP header variant that reports violations without blocking, safe for gradual rollout.