Skip to content
fearchitect
Security

CSP & Trusted Types

Block script injection and DOM XSS at the browser's policy layer.

By Abas TurabliReviewed

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 angle

Content 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.

Next.js middleware — nonce CSP + Trusted Typests
// 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.

Trusted Types policy wrapping DOMPurifyts
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;  ← blocked

The 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.

Further reading

Search fearchitect

Jump to a topic, mode, or action.