CSP (Content Security Policy)

Security

CSP (Content Security Policy) is an HTTP response header telling the browser which origins a page may load each resource type from and what script may execute. Browser-enforced defence-in-depth against XSS and content injection — not a WAF, and no replacement for output encoding.

Also known as Content Security Policy.

13 min read Updated Aug 30, 2026

Full Explanation

CSP (Content Security Policy) is an HTTP response header. A server uses it to tell the browser which resources a page may load and which code may execute. The current specification is Content Security Policy Level 3, a W3C Working Draft of 13 August 2026. It iterates on Level 2, the W3C Recommendation of 15 December 2016. CSP ships in two forms: Content-Security-Policy (enforced) and Content-Security-Policy-Report-Only (reported, never blocked). CSP is not a server-side filter like a WAF: the browser enforces it on the visitor’s machine, long after the response left your infrastructure. CSP is also not CORS. CORS runs the other way round: a responding server declares which other origins may read it (MDN). CSP, by contrast, is your server declaring what your page may pull in. CSP is also not a first line of defense: “CSP is not intended as a first line of defense against content injection vulnerabilities. Instead, CSP is best used as defense-in-depth. It reduces the harm that a malicious injection can cause, but it is not a replacement for careful input validation and output encoding” (CSP3 section 1). In short, CSP is a per-response allowlist for a page’s own subresources and for script execution. Its strongest form pins script to per-response nonces or content hashes rather than to hostnames. On a CDN, its two hard problems are keeping hostnames in step with your delivery estate, and reconciling per-response nonces with cached HTML.

How it works

The server sends the header, and the browser does the enforcing: “When the user agent receives a Content-Security-Policy header field, it MUST parse and enforce each serialized CSP it contains” (CSP3 section 3.1). A policy is a semicolon-separated list of directives. Most directives take a source list: “sets of strings which identify content that can be fetched and potentially embedded or executed”. A source expression can be a keyword such as 'none' or 'self' (which “match nothing and the current URL’s origin, respectively”), a scheme such as https:, a host such as example.com or *.example.com, a nonce, or a digest (CSP3 section 2.3.1).

Fetch directives “control the locations from which certain resource types may be loaded” (CSP3 section 6.1). There is one directive per resource type: script-src for scripts, style-src for stylesheets, img-src for images, and font-src for fonts. connect-src “restricts the URLs which can be loaded using script interfaces”. It covers “APIs like fetch(), XHR, EventSource, beacon, and a’s ping”, and it “also controls WebSocket connections” (CSP3 section 6.1.2). Whatever you omit is not left unrestricted: default-src “serves as a fallback for the other fetch directives”, so “given default-src 'none'; script-src 'self', script requests will use 'self' as the source list to match against. Other requests will use 'none'” (CSP3 section 6.1.3).

Script gets extra machinery, because script is what CSP is really for. script-src “restricts the locations from which scripts may be executed”. Inline blocks “will be blocked unless every policy allows inline script, either implicitly by not specifying a script-src (or default-src) directive, or explicitly, by specifying 'unsafe-inline', a nonce-source or a hash-source that matches the inline block”. Dynamic evaluation is gated separately: “the following JavaScript execution sinks are gated on the 'unsafe-eval' and 'trusted-types-eval' source expressions: eval(), Function(), setTimeout() with an initial argument which is not callable, setInterval() with an initial argument which is not callable” (CSP3 section 6.1.10).

That leaves two ways to vouch for a specific script instead of for a hostname. A nonce is a random value repeated in the header and in the script tag: “if a server delivers a nonce-source expression as part of a policy, the server MUST generate a unique value each time it transmits a policy”. That value “SHOULD be at least 128 bits long (before encoding)” and “SHOULD be generated via a cryptographically secure random number generator” (CSP3 section 7.1). Both of those are SHOULDs. The uniqueness requirement is the MUST. A hash is instead a digest of the script content itself. The grammar admits three algorithms: “hash-algorithm = \"sha256\" / \"sha384\" / \"sha512\"” (CSP3 section 2.3.1). A nonce needs no knowledge of the script, but it needs a fresh value per response. A hash survives caching untouched, but it changes the moment the script body changes by one byte.

'strict-dynamic' is the escape from hostname lists. The reason it exists is the CDN case. In a script-src or default-src directive it has “two main effects”. One is that “host-source and scheme-source expressions, as well as the 'unsafe-inline' and 'self' keyword-sources will be ignored when loading script”. The other is that “script requests which are triggered by non-'parser-inserted' script elements are allowed”. The spec’s own worked example uses a nonced script on cdn.example.com that then creates a script element pointing at othercdn.not-example.net. The second load succeeds even though that host appears nowhere in the policy, because it was not parser-inserted. The purpose is to let “scripts which are given access to the page via nonces or hashes bring in their dependencies without adding them explicitly to the page’s policy” (CSP3 section 8.2). Note what that also means: propagated trust is transitive and unaudited.

Two directives govern things that are not resource fetches at all. frame-ancestors “restricts the URLs which can embed the resource using frame, iframe, object, or embed”. Where it is present with an enforce disposition, it overrides X-Frame-Options entirely (CSP3 section 6.4.2). base-uri “restricts the URLs which can be used in a Document’s base element” (CSP3 section 6.3.1). That is what stops an injected base tag from silently re-pointing every relative URL on the page, including your script src attributes, at an attacker’s host.

Why it matters for a CDN

The specification’s very first worked example is a CDN example. MegaCorp’s developers “can mitigate the risk of script injection by ensuring that their trusted CDN is the only origin from which script can load and execute” (CSP3 section 1.1.1). The operational consequence: every hostname that actually serves a resource type has to appear in the directive for that type. If cdn.example.com serves your images and your fonts, the policy needs img-src cdn.example.com and font-src cdn.example.com. That is because a host source expression matches only “any resource on the host” it names (CSP3 section 2.3.1). Change vendor, add a second vendor for multi-CDN delivery, or spin up a new zone hostname: every directive that named the old host has to be revised in step. Serving CDN assets from your own domain through a CNAME avoids that churn, because 'self' keeps matching “the current URL’s origin” whoever is behind it.

The sharper CDN problem is caching against nonces. A nonce must be new on every response (CSP3 section 7.1), so a nonce policy presumes the HTML is generated per request. Cache that HTML at the edge, and every visitor gets one stored copy of the response, header included. That means one nonce for everybody, which is precisely the property a nonce depends on not having. Google’s guidance splits on exactly this line: “Use a nonce-based CSP for HTML pages rendered on the server. For these pages, you can create a new random number for every response”, versus “Use a hash-based CSP for HTML pages served statically, or pages that need to be cached” (web.dev). If a nonce policy has to stay on cacheable HTML, the remaining route is to mint the nonce at the edge per request. Cloudflare’s Workers example does that with HTMLRewriter: “generate a nonce per request and inject it into both the Content-Security-Policy header and each inline script tag” (Workers example). That makes CSP a reason to run an edge function at all.

Third-party edge features push in the same direction. Real user monitoring beacons, bot-detection scripts, and edge-injected analytics all add script and connection targets you did not author. Each one either needs its host enumerated, or needs to be reachable through a nonced loader plus 'strict-dynamic' (CSP3 section 8.2). That is the practical argument for a nonce or hash policy over an allowlist on a CDN-fronted site. The policy stops describing your delivery topology, so it stops needing a change every time the topology does.

What CDNs do

CSP is a configuration item on every CDN below. It is never a default on any of them. It is not in Cloudflare’s one-click security headers set, nor in CloudFront’s managed SecurityHeadersPolicy. Both of those enumerate exactly what they add. Each mechanism here writes the header on responses at the edge, usually alongside companions such as HSTS.

  • Cloudflare. Response Header Transform Rules can “set the value of an HTTP response header to a literal string value” or “according to an expression”, adding the header if absent (Transform Rules). The one-click “Add security headers” managed transform does not cover CSP. Its documented set is x-content-type-options: nosniff, x-xss-protection: 1; mode=block, x-frame-options: SAMEORIGIN, referrer-policy: same-origin, and expect-ct: max-age=86400, enforce (managed transforms reference). Per-request nonces need a Worker (Workers example).
  • Fastly. A custom VCL snippet sets resp.http.Content-Security-Policy in vcl_deliver. This covers cache hits as well as origin responses: “deliver happens on every response individually, including responses delivered from cache and those received from a backend” (vcl_deliver reference).
  • Akamai. Property Manager’s “Modify Outgoing Response Header” behavior adds the header. It does this “after the response is processed by the Akamai server and retrieved either from the customer origin or from cache, immediately before it is sent to the requesting client”, so “you can add a header that is specific to the user making the request, without affecting the cached object”. The similarly named “Modify Incoming Response Header” behavior is the trap. It “adds the response header before the object is processed by the Akamai server, so if the response is cacheable, the header will be stored with the cached object”. A nonce set there would be shared by every cache hit (Property Manager).
  • Amazon CloudFront. A response headers policy attached to one or more cache behaviors adds the Content-Security-Policy directives you declare: “making these changes doesn’t require writing code or changing the origin”, and it applies to “the responses that it serves from the cache and the ones that it forwards from the origin” (response headers). The managed SecurityHeadersPolicy is not one of them. It lists only Referrer-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, and X-XSS-Protection (managed policies). So CSP means a custom policy whose ContentSecurityPolicy property holds “the policy directives and their values that CloudFront includes as values for the Content-Security-Policy HTTP response header” (CloudFormation reference).

Watch out for

  • Report-only enforces nothing. Content-Security-Policy-Report-Only “allows web developers to experiment with policies by monitoring (but not enforcing) their effects” (CSP3 section 3.2). Violations are reported. Nothing is blocked. It can only be delivered as a header, not in a meta element. The same is true for report-uri, frame-ancestors, and sandbox: header delivery only (CSP3 section 3.2, 3.3).
  • report-uri is deprecated. “The report-uri directive is deprecated. Please use the report-to directive instead. If the latter directive is present, this directive will be ignored”, and the spec suggests specifying both for backwards compatibility (CSP3 section 6.5.1). report-to does not take a URL. It names a group declared in the separate Reporting-Endpoints response header (MDN).
  • Host allowlists keep getting bypassed. An allowlist CSP such as script-src www.googleapis.com “require[s] a lot of customization and can be bypassed by attackers” (web.dev). The spec agrees: “host- and path-based policies are tough to get right, especially on sprawling origins like CDNs”. These policies produce lists that “end up being brittle, awkward, and difficult to implement and maintain” (CSP3 section 8.2).
  • The escape hatches are the holes. 'unsafe-inline' re-opens inline script. 'unsafe-eval' re-opens the eval sinks. A source list only allows all inline behaviour if it contains 'unsafe-inline' and does not override it. The algorithm returns “Does Not Allow” as soon as any expression “matches the nonce-source or hash-source grammar” (CSP3 section 6.7.3.2). That is why web.dev notes that “all recent browsers ignore unsafe-inline if a CSP nonce or hash is present” (web.dev). Drop the nonce, and the hatch swings open again.
  • A nonce is a shared secret, and strict CSP is not total. Nonces “override the other restrictions present in the directive in which they’re delivered”, so “an attacker who can gain access to the nonce can execute whatever script they like, whenever they like” (CSP3 section 7.1). Even a correct strict policy does not help “if you nonce a script, but there’s an injection directly into the body or the src parameter of that script element”. It also does not help against injections into the locations of dynamically created scripts (web.dev).
  • Missing directives break pages quietly. Any resource type you did not name inherits default-src (CSP3 section 6.1.3). So with default-src 'none', a font host, a RUM beacon absent from connect-src, or an API call to a hostname you forgot simply fails. The only trace is a console message on the visitor’s machine. Set the header on every response, not only the document (MDN), and re-derive the hostname list whenever the delivery estate changes.

Best practice

  • Deploy in report-only first, and keep it there until the violation stream is clean. Both headers can ride the same response: “the policy specified in Content-Security-Policy headers is enforced while the Content-Security-Policy-Report-Only policy generates reports but is not enforced”. That means you can enforce today’s policy while testing tomorrow’s (MDN).
  • Build on Strict CSP. The spec identifies it as “an effective and deployable mitigation against XSS”. It names two directives. For script-src: “only use nonce source-expression and/or hash source-expression with the 'strict-dynamic' keyword-source”. For base-uri: “specify a value of either 'self' or 'none'”. Two caveats come from the spec itself. 'strict-dynamic' “should be avoided when possible”, because it is a deployment convenience, and “for backwards compatibility, it is recommended to specify https: scheme-source with 'strict-dynamic'” (CSP3 section 8.5). Google’s version of the same recipe adds object-src 'none' to disable plugin content: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none' (web.dev).
  • Match the policy style to the cache. Server-rendered HTML gets nonces. Statically served or cached HTML gets hashes (web.dev). Only keep a nonce policy on cacheable HTML if an edge function regenerates the nonce per request. Set it in the outgoing-response stage so it is not stored with the cached object.
  • Prefer nonces and hashes over hostnames, so the policy does not encode your delivery topology. Where a hostname is unavoidable, name it once per resource type. Regenerate the list deliberately on any vendor, zone, or CNAME change.
  • Report through the Reporting API. Declare Reporting-Endpoints, and point report-to at a group. Keep report-uri alongside until report-to is universally supported (MDN).
  • Verify what the edge actually emits, not what the configuration says: run curl -sI against a cache hit and a cache miss. Treat the whole thing as one layer: “setting a CSP is not an alternative to sanitizing input. Websites should sanitize input and set a CSP, providing defense in depth against XSS” (MDN).

Examples

# Basic CSP header
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' cdn.example.com data:;

# Strict CSP with nonces (recommended)
Content-Security-Policy: default-src 'self'; script-src 'nonce-abc123' 'strict-dynamic'; style-src 'self';

# In your HTML
<script nonce="abc123" src="/app.js"></script>

# Report-only mode (deploy this first)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports;

# Nginx: add CSP header
add_header Content-Security-Policy "default-src 'self'; script-src 'self' cdn.example.com; img-src 'self' cdn.example.com;" always;

# Check a site's CSP
curl -sI https://example.com | grep -i content-security-policy

# Common CDN-friendly policy
Content-Security-Policy: default-src 'self'; script-src 'self' 'strict-dynamic' 'nonce-{random}'; style-src 'self' 'unsafe-inline'; img-src 'self' *.cdn.example.com; connect-src 'self' api.example.com; font-src 'self' fonts.googleapis.com;

Frequently Asked Questions

CSP (Content Security Policy) is an HTTP response header telling the browser which origins a page may load each resource type from and what script may execute. Browser-enforced defence-in-depth against XSS and content injection — not a WAF, and no replacement for output encoding.

# Basic CSP header
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' cdn.example.com data:;

# Strict CSP with nonces (recommended)
Content-Security-Policy: default-src 'self'; script-src 'nonce-abc123' 'strict-dynamic'; style-src 'self';

# In your HTML
<script nonce="abc123" src="/app.js"></script>

# Report-only mode (deploy this first)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports;

# Nginx: add CSP header
add_header Content-Security-Policy "default-src 'self'; script-src 'self' cdn.example.com; img-src 'self' cdn.example.com;" always;

# Check a site's CSP
curl -sI https://example.com | grep -i content-security-policy

# Common CDN-friendly policy
Content-Security-Policy: default-src 'self'; script-src 'self' 'strict-dynamic' 'nonce-{random}'; style-src 'self' 'unsafe-inline'; img-src 'self' *.cdn.example.com; connect-src 'self' api.example.com; font-src 'self' fonts.googleapis.com;

Yes. CSP (Content Security Policy) is also known as Content Security Policy. CSP (Content Security Policy) is an HTTP response header telling the browser which origins a page may load each resource type from and what script may execute. Browser-enforced defence-in-depth against XSS and content injection — not a WAF, and no replacement for output encoding.

Related CDN concepts include:

  • WAF (WAF) — Web application firewall: a reverse proxy that inspects HTTP(S) requests against rule sets and blocks …