TL;DR / CSP is an HTTP response header that instructs the browser to restrict which resources (scripts, styles, images, etc.) a page is allowed to load, providing a strong defense-in-depth layer against XSS and data injection attacks.
How It Works
┌───────────────┐
│ HTTP Response │
└───────────────┘
│
│
↓
┌────────────────────┐
│ CSP Header │
│ Parse directives │
│ │
└────────────────────┘
│
│
┌───────└──────────────────────┐
│ │ │
↓ ↓ ↓
┌────────────┐ ┌───────────┐ ┌─────────┐
│ script-src │ │ style-src │ │ img-src │
└────────────┘ └───────────┘ └─────────┘
│
┌┘
│
↓
┌──────────┐
│ Allowed? │ YES: load
└──────────┘ NO: block + report
Content Security Policy works by declaring a whitelist of approved content sources for each resource type. The browser enforces this policy on every resource load, blocking anything that violates it. CSP does not replace proper output encoding -- it is a second line of defense that limits the damage when other protections fail.
Directive Structure
A CSP header contains one or more directives, each controlling a specific resource type. script-src controls JavaScript execution, style-src controls CSS, img-src controls images, connect-src controls fetch/XHR destinations, frame-src controls iframe sources, and default-src acts as the fallback for any directive not explicitly set.
Each directive accepts source expressions: 'self' (same origin), specific domains (cdn.example.com), scheme restrictions (https:), or the special keywords 'unsafe-inline', 'unsafe-eval', 'nonce-<value>', and 'sha256-<hash>'.
Nonce-Based CSP (Strict CSP)
The most effective modern CSP strategy uses nonces. The server generates a random value per request and sets script-src 'nonce-abc123'. Every legitimate script tag includes nonce="abc123". Injected scripts lack the nonce and are blocked. This approach eliminates the need to maintain domain whitelists and works even with CDN-hosted scripts.
Google's recommended strict CSP looks like: script-src 'nonce-{random}' 'strict-dynamic'; base-uri 'self'; object-src 'none'. The strict-dynamic keyword allows scripts loaded by a nonced script to execute without their own nonce, enabling script loaders and bundlers to work naturally.
Hash-Based CSP
For pages with known, static inline scripts, hash-based CSP specifies the exact SHA-256 (or SHA-384/512) hash of each allowed script. The browser computes the hash of any inline script and checks it against the policy. This is effective for static pages but impractical when inline scripts contain dynamic values, since any change invalidates the hash.
Report-Only Mode
Content-Security-Policy-Report-Only applies the same parsing and evaluation but logs violations instead of blocking them. This is essential for rolling out CSP on existing applications. You deploy in report-only mode, collect violation reports via the report-uri or report-to directive, fix the violations, and then switch to enforcement mode. Skipping this step invariably breaks production functionality.
Violation reports are JSON payloads sent to your specified endpoint containing the blocked URI, the violated directive, the document URI, and the source file/line number. These reports are invaluable for identifying inline scripts, third-party resources, and other policy violations before they impact users.
Key Directives Beyond Scripts
connect-src is often overlooked but controls where fetch(), XMLHttpRequest, WebSockets, and EventSource can connect. Without it, an XSS payload could exfiltrate data to an attacker's server even if script injection is limited. form-action restricts where forms can submit, preventing form hijacking. base-uri restricts the <base> element, which otherwise could redirect all relative URLs to an attacker's domain. frame-ancestors controls who can embed your page in an iframe, replacing the older X-Frame-Options header.
CSP Level 3 Features
CSP Level 3 introduced strict-dynamic, which propagates trust to dynamically loaded scripts. It also added the report-to directive using the Reporting API for more structured violation delivery. The worker-src directive controls Web Worker and Service Worker origins, closing a gap that attackers previously exploited to run scripts through worker contexts.
Gotchas
unsafe-inlineinscript-srcnegates most XSS protection. If you need it, your CSP provides minimal security value. Migrate to nonces or hashes instead.unsafe-evalallowseval(),Function(),setTimeout('string'), and similar constructs. Many libraries require it, but it dramatically weakens your CSP. Audit dependencies before adding it.default-srcdoes not coverframe-ancestors,base-uri, orform-action. These must be set explicitly or they have no restriction.- Third-party scripts that inject further scripts break nonce-based CSP unless you use
strict-dynamic. Without it, every transitively loaded script needs its own nonce. - CSP is per-response, not per-application. Every HTML response must include the header. Forgetting it on an error page or admin route creates a gap an attacker can target.