Content Security Policy (CSP) is a response header that tells the browser which scripts, styles, images, frames and connections a document may use. If an attacker slips <script> or <img onerror=...> into your HTML, a good policy stops it from executing. A policy is a semicolon-separated list of directives, each with a source list. CSP has been Baseline Widely available since 2016; the current specification is CSP Level 3.
| Directive | What it controls |
|---|---|
| default-src | Fallback for every fetch directive not listed |
| script-src, style-src, img-src, font-src | Scripts, stylesheets, images, fonts |
| connect-src | fetch(), XHR, WebSocket, EventSource |
| frame-src, object-src | Frames; <object> and <embed> |
| base-uri, form-action | <base href>; where forms may submit |
| frame-ancestors | Which sites may frame this page (clickjacking) |
| report-to | Reporting endpoint for violations |
| require-trusted-types-for | DOM XSS sinks (Sandboxing and Trusted Types) |
Sources are keywords in single quotes or bare hosts: 'self' (same origin), 'none', https://cdn.example, *.example.com, https:, 'nonce-…' and 'sha256-…' (nonce), 'unsafe-inline' (any inline code), 'unsafe-eval' (eval() and new Function()) and 'wasm-unsafe-eval' (WebAssembly only). Older references list plugin-types and navigate-to; neither is in Level 3, and report-uri is deprecated in favor of report-to.
Allowlists fail; strict policies work
Early policies listed trusted hosts, such as script-src 'self' 'unsafe-inline' https://apis.google.com 1 .... In 2016 Google engineers analyzed 26,011 distinct policies from 1.68 million hosts and found 94.72% of them bypassable: 'unsafe-inline' lets injected code run, and big allowlisted domains host JSONP endpoints and old library versions that attackers can repurpose. The same paper introduced 'strict-dynamic', the basis of today's recommended strict CSP:
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only:
script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none'; base-uri 'none'; frame-ancestors 'self';
report-to csp; report-uri https://example.com/csp-reportsThe header is sent on one line; it is wrapped here for reading. The nonce approves your scripts, and 'strict-dynamic' extends that trust to scripts they create with document.createElement('script'), so tag managers and lazy-loaded bundles work without a host list. https: and 'unsafe-inline' are fallbacks for old browsers: any browser that understands nonces ignores 'unsafe-inline', and one that understands 'strict-dynamic' ignores https:. object-src 'none' disables plugins, base-uri 'none' stops an injected <base> from redirecting relative script URLs, and frame-ancestors replaces X-Frame-Options.
Roll out in stages: send the -Report-Only header, collect reports, fix your inline handlers, then enforce. Helmet 10,736 (https://github.com/helmetjs/helmet 10,736 ) (MIT, npm 2,036 install helmet) sets CSP and a dozen other security headers in Express 24,430 with app.use(helmet()), and Google's CSP Evaluator 659 (https://csp-evaluator.withgoogle.com/ 659 ) (Apache-2.0) flags bypassable allowlists before you deploy. A frequent interview question asks why 'unsafe-inline' in script-src is pointless against XSS; the answer is that a typical XSS payload is inline script.