typescript 102 lines · 3 tabs

Content Security Policy headers (defense-in-depth)

Shared by codesnips Jan 2026
3 tabs
import { randomBytes } from 'crypto';
import { Request, Response, NextFunction } from 'express';

interface CspOptions {
  reportOnly?: boolean;
  reportUri?: string;
}

function buildPolicy(nonce: string, reportUri?: string): string {
  const directives = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data:",
    "connect-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'",
    "form-action 'self'",
    'upgrade-insecure-requests',
  ];
  if (reportUri) directives.push(`report-uri ${reportUri}`);
  return directives.join('; ');
}

export function cspMiddleware(options: CspOptions = {}) {
  const header = options.reportOnly
    ? 'Content-Security-Policy-Report-Only'
    : 'Content-Security-Policy';

  return (req: Request, res: Response, next: NextFunction): void => {
    const nonce = randomBytes(16).toString('base64');
    res.locals.cspNonce = nonce;
    res.setHeader(header, buildPolicy(nonce, options.reportUri));
    next();
  };
}
3 files · typescript Explain with highlit

A Content Security Policy (CSP) is a browser-enforced allowlist that tells the page which sources of script, style, images, and connections are trusted. It is a defense-in-depth control: even if an attacker manages to inject markup through an XSS hole, a strict policy stops the injected <script> from executing because it lacks the correct origin or nonce. This snippet builds a nonce-based CSP for an Express app so that inline scripts the server actually emits are permitted while any injected inline script is rejected.

In csp.ts, cspMiddleware generates a fresh 128-bit random nonce per request with randomBytes(16), base64-encodes it, and stashes it on res.locals.cspNonce so templates can read it. The policy string is assembled from buildPolicy, which pins default-src to 'self', allows scripts only from 'self' plus the request nonce, and adds hardening directives like object-src 'none', base-uri 'self', and frame-ancestors 'none' to block plugin execution, base-tag hijacking, and clickjacking. The 'strict-dynamic' token lets a nonced loader script pull in further scripts without needing every URL enumerated, which keeps the policy maintainable as bundles change.

The key trade-off is enforce-versus-observe. Shipping a broken policy can break a live site, so csp.ts supports a reportOnly flag that switches the header name to Content-Security-Policy-Report-Only. In that mode the browser reports violations to the report-uri endpoint but does not block anything, which is the safe way to roll a policy out and tune it against real traffic before enforcing.

In report.ts, cspReportHandler accepts the application/csp-report beacon the browser POSTs on each violation, normalizes the payload, and logs the blocked URI and violated directive. Filtering noisy blocked-uri values such as browser-extension origins keeps the signal clean.

In app.ts, the middleware is mounted before the routes and the JSON body parser is scoped to the report route with a matching content type. res.locals.cspNonce flows into the rendered template's inline script tags. Pitfalls to watch: nonces must never be reused across responses, unsafe-inline is ignored by browsers whenever a nonce is present, and any third-party widget needs an explicit source entry or it will silently fail under enforcement.


Related snips

Share this code

Here's the card — post it anywhere.

Content Security Policy headers (defense-in-depth) — share card
Link copied