CSP EXAMPLES AND ROLLOUT

Content-Security-Policy examples: how to build a strict CSP for a modern site

A Content-Security-Policy tells the browser which scripts may run on your pages. Three working example policies, the parts that make a policy strict (nonces, hashes and strict-dynamic), the directives default-src does not cover, violation reporting and a staged rollout that starts in Report-Only mode.

Concept illustration of an ivory ceramic gatehouse with a glass portcullis: glass spheres carrying a mint seal pass through and glow, while one grey sphere without a seal stops outside next to a copper stamp.
Concept illustration

A Content-Security-Policy (CSP) is a response header that tells the browser which scripts, styles, images, frames and connections a page may use. If an attacker manages to slip a script into your HTML, a good policy stops the browser from running it. A policy copied from a forum thread usually goes wrong in one of two ways: it breaks analytics, web fonts and the payment widget, or it gets padded with 'unsafe-inline' and wildcards until it allows almost everything.

This guide gives working examples for three kinds of sites, explains what makes a policy strict (nonces, hashes and 'strict-dynamic'), covers the directives that default-src does not reach, shows how to collect violation reports and lays out a rollout that starts in Report-Only mode, so nothing breaks without warning. All examples use example.com; replace the placeholder values with your own.

Scope of this guide

The free checker reads the headers of one public response: the home page of the origin you enter, after up to five redirects. It reads only the enforced Content-Security-Policy header, not a Report-Only policy, does not judge whether the policy fits your site, and is not a pentest.

1. What a Content-Security-Policy does, and what it does not

A policy is a list of directives separated by semicolons. Each directive names a resource type and the sources allowed for it: script-src for JavaScript, style-src for CSS, img-src, font-src, connect-src for fetch, XHR and WebSocket connections, and frame-src for frames the page embeds. default-src is the fallback for these fetch directives when a specific one is missing (W3C CSP Level 3).

Three important directives do not fall back to default-src: frame-ancestors (which sites may frame your page), base-uri (what a <base> element may set) and form-action (where forms may submit). A policy of default-src 'self' alone therefore still lets any site frame the page and lets an injected <base> tag redirect relative URLs.

Source values come in three kinds:

  • Keywords in single quotes: 'self' (the page’s own origin), 'none' (nothing), 'unsafe-inline', 'unsafe-eval' and 'strict-dynamic'.
  • Hosts and schemes: https://cdn.example.com, https://*.example.com, https: or data:.
  • Nonces and hashes: 'nonce-…' and 'sha256-…', which allow individual script or style elements rather than whole hosts.

Send the policy as an HTTP header. A <meta http-equiv="Content-Security-Policy"> tag works for most directives, but the specification excludes frame-ancestors, report-uri and sandbox there, and a Report-Only policy cannot be delivered in a meta tag at all (MDN CSP guide). Keep expectations realistic: a CSP limits what injected code can do. It does not replace escaping output and validating input, which stop the injection in the first place.

2. Three Content-Security-Policy examples to start from

Example A: a site without third-party scripts. An allowlist policy works when every script and stylesheet is a file on your own origin:

Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests

It blocks every inline <script>, every style attribute and anything loaded from other hosts, so it suits hand-built or statically generated sites with no embeds.

Example B: a server-rendered site with a nonce. This is the strict policy that web.dev recommends for pages the server builds per request, plus framing protection:

Content-Security-Policy: script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'

The nonce changes on every response (section 3). Note what it leaves out: with no default-src, styles, images, fonts and connections are not restricted. That is a deliberate trade-off: it concentrates on script execution, where most of the damage from cross-site scripting happens, and avoids host lists that break whenever a vendor changes domains.

Example C: static or cached pages with hashes. When the same HTML is served to everyone, allow inline scripts by their hash instead:

Content-Security-Policy: script-src 'sha256-BASE64_HASH_OF_INLINE_LOADER' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'
Policy styleGood fitOngoing workWatch out for
A: host allowlistStatic sites with no third-party codeUpdate the host list when you add a vendorAny script on an allowed host can be loaded, including old library versions on a shared CDN
B: nonce + strict-dynamicPages rendered per request by your applicationAdd the nonce to every script tag in your templatesPage caches that replay one nonce to every visitor
C: hash + strict-dynamicStatic, prerendered or edge-cached HTMLRecompute hashes whenever inline code changesMinifiers and templates that change whitespace and therefore the hash

Whichever you pick, add the reporting directives from section 6 and send it first as Content-Security-Policy-Report-Only.

3. Nonces and hashes instead of 'unsafe-inline'

'unsafe-inline' allows every inline script, including one an attacker injects, which removes most of what CSP offers against cross-site scripting. Nonces and hashes allow only the inline code you meant to ship. Browsers ignore 'unsafe-inline' in a directive that also contains a nonce or a hash (MDN), which is why it can stay in a strict policy as a fallback for very old browsers.

Nonces

A nonce is a random value that the server generates for each response, puts in the header and copies into the nonce attribute of every script it intends to run. It must be unpredictable and new on every response; web.dev recommends at least 128 bits from a cryptographically secure generator. In a Node.js application:

import crypto from 'node:crypto';

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;
  res.setHeader('Content-Security-Policy',
    `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'`);
  next();
});

The template then writes <script nonce="…" src="/app.js"></script> with the same value. Add the nonce where your templates write your own script tags, never by rewriting every <script> in the finished HTML: a blanket rewrite would also approve a script an attacker injected. Two traps are common: a CDN or page cache that stores the HTML replays the same nonce to every visitor, and a value baked into a static file at build time is not a nonce at all.

Hashes

A hash allows one exact inline script: the base64-encoded SHA-256, SHA-384 or SHA-512 digest of the text between the script tags. Every character counts, including spaces and line breaks, so a minifier or template change produces a new hash. That makes hashes the better fit for static and cached pages, where the content is the same for every response. Chrome prints the expected hash in the console when it blocks an inline script, or you can compute it yourself:

printf '%s' 'console.log("hello")' | openssl dgst -sha256 -binary | openssl base64

What neither of them allows

Inline event handlers such as onclick="…" and javascript: URLs are blocked by a nonce- or hash-based policy. Move that code into script files and attach it with addEventListener. 'unsafe-hashes' can allow one specific handler by its hash, but treat it as a stopgap. Code that runs strings as code, such as eval(), new Function() or setTimeout with a string, needs 'unsafe-eval'; replace it where you can, and use the narrower 'wasm-unsafe-eval' if only WebAssembly needs compiling (MDN script-src).

4. strict-dynamic: letting trusted scripts load what they need

Most sites run scripts that load more scripts: a tag manager adds analytics, a consent banner adds its vendors, a chat widget pulls in its own bundle. Listing every host they might use is fragile. 'strict-dynamic' takes another route: a script allowed by a nonce or hash may add further scripts, and those are trusted as well.

In browsers that support it, 'strict-dynamic' also makes the browser ignore host allowlists, 'self' and 'unsafe-inline' in script-src (MDN). Three consequences follow:

  • Every <script> tag in your HTML needs the nonce or a matching hash, including scripts from your own origin, because 'self' no longer allows them.
  • Scripts that a trusted script creates with document.createElement('script') are allowed. Scripts written into the page with document.write (parser-inserted scripts) are not, so old embed snippets that rely on it break.
  • Trust is passed on. Whatever your tag manager loads will run, so everyone who can publish in the tag manager can effectively run code on your site. Treat that access like deploy access.

For older browsers, add fallbacks that newer browsers ignore:

script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none'

A browser that supports CSP Level 3 applies only the nonce and 'strict-dynamic'. A browser that knows nonces but not 'strict-dynamic' uses the nonce and https:, and a very old browser falls back to 'unsafe-inline' https:. Google documents a nonce-aware Tag Manager snippet that passes the nonce on to the scripts it adds, and notes that custom JavaScript variables in Tag Manager still need 'unsafe-eval' (Tag Manager CSP guide).

5. frame-ancestors, object-src 'none' and base-uri: the directives to add explicitly

  • frame-ancestors decides which sites may embed your page in a frame, the defense against clickjacking. Use 'none' if nobody needs to frame the page, 'self' if only your own pages do, or list exact origins such as https://app.example.com. It works only in the HTTP header. X-Frame-Options (DENY or SAMEORIGIN) remains a fallback for older browsers; make both allow the same framing (MDN frame-ancestors).
  • object-src 'none' blocks <object> and <embed>. Plugins are gone from modern browsers, but these elements can still load content, and a strict policy without default-src leaves them unrestricted unless you say so.
  • base-uri 'none', or 'self' if your pages use a <base> element, stops an injected <base href> from pointing every relative script URL at another host.
  • form-action 'self' limits where forms may submit. Add the origin of your payment or sign-in provider if a form posts there, and test those flows after the change.
  • upgrade-insecure-requests makes the browser fetch http:// subresources over HTTPS. It helps with mixed content but does not replace HSTS.

Write these directives into every policy explicitly, including examples B and C above. They need almost no maintenance and close gaps that the script rules leave open.

6. Reporting violations with report-to and report-uri

Violation reports tell you what a policy blocked, or would block in Report-Only mode, in real visitors’ browsers. CSP Level 3 defines report-to, which names an endpoint declared in the Reporting-Endpoints header, and deprecates the older report-uri, which takes a URL directly. MDN lists report-to as newly available across current browsers since 2026, while older browser versions understand only report-uri. Browsers that support report-to ignore report-uri, so send both for now (MDN report-to):

Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic' 'report-sample'; object-src 'none'; base-uri 'none'; report-uri https://example.com/csp-reports; report-to csp

The two mechanisms post different formats. report-uri sends a JSON object with a csp-report key and the content type application/csp-report. report-to sends application/reports+json: an array of reports whose type is csp-violation and whose body contains fields such as documentURL, blockedURL, effectiveDirective and disposition (enforce or report). With 'report-sample', a report for blocked inline code includes its first 40 characters. Accept both formats on the same endpoint.

  • Expect noise. Browser extensions inject scripts and trigger violations with source files such as chrome-extension:// or moz-extension://. Group reports by directive and blocked host and look for patterns rather than single entries.
  • Treat reports as personal data. Document URLs and referrers can carry query strings with email addresses or tokens. Strip or shorten them, keep reports only for a short period and do not pass them on without a reason.
  • Protect the endpoint. Anyone can post fake reports, so apply size and rate limits, and never show report content unescaped in an admin view.

7. What usually breaks: analytics, fonts, inline scripts and embeds

Most violations in the first days come from a handful of sources. The report’s effectiveDirective tells you which rule to look at:

SymptomDirective in the reportTypical fix
Analytics stops recording page viewsscript-src-elem, connect-src, img-srcLoad the tag with a nonce, or allow its script host, and allow the collection hosts the vendor documents (Google lists them per product)
Web fonts fall back to system fontsfont-src, style-src-elemAllow the stylesheet host in style-src and the font file host in font-src (for Google Fonts, fonts.googleapis.com and fonts.gstatic.com), or self-host the files
A theme or CMS snippet stops workingscript-src-elem with blockedURL “inline”Add the nonce in the template, move the code into a file or allow its hash
Buttons with onclick do nothingscript-src-attrRewrite the handler with addEventListener in a script file
Tag manager custom variables return nothingscript-src (eval)Replace them with built-in variables, or accept 'unsafe-eval' as a recorded exception
An embedded video, map or payment frame stays blankframe-srcAdd the provider’s exact origin
Inline styles are ignored and the layout shiftsstyle-src-elem, style-src-attrMove styles into stylesheets, add the nonce to style elements, or keep 'unsafe-inline' in style-src only, as a recorded exception
Chat or live updates fail to connectconnect-srcAdd the vendor’s https:// and wss:// hosts
Your app or a partner can no longer embed the pageframe-ancestorsList the exact origins that embed it

Inline styles are a smaller risk than inline scripts, but not harmless: injected CSS can change what visitors see and, with attribute selectors, leak some page data. Many frameworks and CSS-in-JS libraries insert style elements at runtime, so check whether yours supports a nonce before removing 'unsafe-inline' from style-src. Self-hosting fonts and libraries removes several of these rows at once and keeps the policy short.

8. Rolling out a CSP in stages

  1. Inventory. List the scripts, styles, fonts, frames and connection targets on your key pages: home, sign-in, account, checkout and pages with embedded widgets. The DevTools Network panel and your tag manager configuration are the quickest sources.
  2. Report-Only. Send the draft as Content-Security-Policy-Report-Only with reporting. Nothing is blocked; violations appear in the console and at your endpoint. Leave it running long enough for less-visited pages and campaigns to show up, usually a week or more.
  3. Fix the sources, not the policy. Add nonces, move inline handlers into files, self-host fonts. Add a host or 'unsafe-eval' only as a recorded exception with an owner and a reason.
  4. Enforce. Switch the header name to Content-Security-Policy. Keep a stricter Report-Only policy running beside it to test the next tightening: when both headers arrive, the browser enforces one and only reports on the other.
  5. Keep watching. A new marketing tag, a plugin update or a framework upgrade can bring new inline code. Keep the reporting endpoint running and check the header again after every server, CDN or framework change.

Two infrastructure details cause many surprises. If a CDN or framework adds its own CSP header, the browser enforces every policy it receives and a resource must pass all of them, so an extra header can only make things stricter, never looser (MDN). And if HTML is cached at the edge, a nonce-based policy needs the nonce generated where the page is assembled, or a switch to hashes. To see what a page really sends:

curl -sS -D - -o /dev/null https://example.com/ | grep -i content-security-policy

The security headers check guide covers the headers that belong next to CSP, and the website launch security checklist puts them into a full pre-launch review.

9. Checking the deployed header with the free security headers checker

Once the policy is enforced, the free security headers checker shows what a browser receives from your home page. It runs the free Launch Readiness Snapshot, eight passive checks on one public domain without signup, and opens the header check above the other seven.

  • It requests the home page of the origin you enter, follows up to five redirects and reads the final response. Other pages, such as sign-in or checkout, often carry different policies; check those with DevTools or curl.
  • It reads only the enforced Content-Security-Policy header. During the Report-Only stage it still reports CSP as missing, rated medium, because a report-only policy blocks nothing. That result is expected until you enforce.
  • A frame-ancestors directive in the enforced policy counts as framing protection, just like X-Frame-Options.
  • In the separate hardening grade (A to F), a CSP earns less credit when default-src or a script-src or style-src directive (including the -elem and -attr variants) contains 'unsafe-inline', or when default-src or a script directive contains 'unsafe-eval'. The grade reads the policy as written, so an 'unsafe-inline' fallback next to a nonce also lowers the credit, even though current browsers ignore it.
  • It does not evaluate nonces, hashes, 'strict-dynamic', object-src, base-uri or reporting. Use the browser console and your violation reports for those.

The result shows a score and the result of each of the eight checks, so the CSP signal sits next to HSTS, TLS and the other public checks.

Common questions

What is a good Content-Security-Policy example to start with?

For a server-rendered site: script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self', with a new nonce on every response. For static or cached pages, use hashes of your inline scripts instead of the nonce. Start either one as Content-Security-Policy-Report-Only.

Should I use a nonce or a hash in my CSP?

Use a nonce when your application builds each page, because it can insert a fresh value every time. Use hashes when the HTML is static or cached, because a cached nonce is reused for every visitor. Both work together with 'strict-dynamic'.

Is 'unsafe-inline' in style-src a problem?

It is a smaller risk than 'unsafe-inline' in script-src, but injected CSS can still change what visitors see and leak some page data. Keep it only as a recorded exception while you move styles into stylesheets or add nonces to style elements.

Does Content-Security-Policy-Report-Only protect my site?

No. It blocks nothing; it only reports what an enforced policy would block. Use it to test, then switch to the Content-Security-Policy header. Both headers can be sent together, which is useful for testing the next, stricter version.

Can I set a CSP in a meta tag?

Yes, for most directives. But frame-ancestors, report-uri and sandbox are ignored in a meta tag, and a Report-Only policy cannot be delivered there. Use the HTTP header wherever your server or host lets you set one.

What is the difference between report-uri and report-to?

report-uri takes a URL directly; CSP Level 3 deprecates it, but older browsers still rely on it. report-to names an endpoint from the Reporting-Endpoints header and uses the Reporting API format. Browsers that support report-to ignore report-uri, so sending both is fine.

Sources & further reading

  1. W3C: Content Security Policy Level 3www.w3.org
  2. MDN: Content Security Policy (CSP) guidedeveloper.mozilla.org
  3. MDN: Content-Security-Policy headerdeveloper.mozilla.org
  4. MDN: CSP script-src directivedeveloper.mozilla.org
  5. MDN: CSP frame-ancestors directivedeveloper.mozilla.org
  6. MDN: CSP report-to directivedeveloper.mozilla.org
  7. MDN: Reporting-Endpoints headerdeveloper.mozilla.org
  8. W3C: Reporting APIwww.w3.org
  9. web.dev: Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)web.dev
  10. Google Tag Platform: Use Tag Manager with a Content Security Policydevelopers.google.com
  11. OWASP Content Security Policy Cheat Sheetcheatsheetseries.owasp.org
Sitelemetry team

Prepared by the Sitelemetry editorial team. Explore the linked sources for further detail.

SITELEMETRY

Put what you learned to work.

Explore your website’s structure, settings and signals with Sitelemetry.