A content security policy (CSP) is an HTTP response header that tells the browser which scripts, styles, images, frames and connections a page may use. If an attacker manages to inject a script into one of your pages, a good policy can stop the browser from running it. That is why CSP is described as defense in depth: it limits the damage from cross-site scripting (XSS) but does not replace fixing the bug that allowed the injection.
The usual reason CSPs fail is not the idea, it is the rollout. Someone pastes a strict policy into production, the analytics tag, the chat widget and half the inline scripts stop working, and the header is quietly removed again. This guide shows a safer order: observe first in report-only mode, fix what the reports show, then enforce.
It is written for site owners and developers who can edit response headers (in a web server, CDN or application). By the end you will have a working policy, a way to receive violation reports, and a short list of habits that keep it from breaking on the next release. If you are still adding the other security headers, read Add HTTP Security Headers: CSP, nosniff, Referrer-Policy and More first or alongside this one.
Before you begin
You need three things: access to where response headers are set, a staging copy of the site if you have one, and a list of everything the site loads. Make that list from the browser's developer tools (Network tab) on your most important templates: home page, article or product page, checkout or contact form. Note every third-party host, such as analytics, fonts, video embeds, payment frames and chat widgets.
Plan for an hour of reading and a few days of watching reports before you enforce anything. An enforced policy that is too strict blocks legitimate code and can break menus, forms and payments, which is why Step 3 uses a header that blocks nothing. Keep a copy of your current headers so you can revert quickly.
CSP also needs a decision about inline code. Many sites have inline script blocks, inline onclick attributes and inline style attributes. A strict policy blocks these by default, so some templates may need editing.
How a policy is built
A policy is a list of directives separated by semicolons. Each directive is a name followed by one or more allowed sources:
Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example.com
The directives you will meet most often:
default-srcis the fallback for the fetch directives you do not set yourself.script-srcandstyle-srccontrol JavaScript and CSS.img-src,font-src,connect-src(fetch, XHR, WebSocket targets) andframe-src(iframes) work the same way.object-src 'none'blocks the old plugin elements<object>and<embed>.base-uri,form-actionandframe-ancestorsdo not fall back todefault-src, so set them explicitly if you want them restricted.frame-ancestorscontrols which pages may embed yours and is the more flexible replacement forX-Frame-Options.
Quoting matters. Keywords such as 'self', 'none' and 'unsafe-inline', and values such as 'nonce-...' and 'sha256-...', must be in single quotes. Hosts and schemes such as https: or images.example.com must not be. Writing script-src self means a host literally named "self".
If several policies reach the browser, each one must allow a resource, so adding a policy can only make things stricter.
Step-by-step instructions
Step 1: Decide how inline code will be allowed
Allowlisting hosts alone is hard to keep correct and is often bypassable, which is why current guidance favors a "strict" CSP built on nonces or hashes instead.
- A nonce is a random value your server generates for every response. You put it in the header and on each allowed script tag. It must be unpredictable and new on every response, so it needs server-side templating and cannot work with fully static, cached HTML.
- A hash is a base64 SHA-256, SHA-384 or SHA-512 digest of an inline script's exact contents, written as
'sha256-...'. It suits static pages, but any change to the script, even whitespace, changes the hash.
Pick nonces for server-rendered pages and hashes for static files.
What success looks like: You know which of the two methods fits each template, and you have a list of inline handlers (onclick attributes and links that use the javascript scheme) to refactor.
Step 2: Draft a starting policy
A common strict starting point, taken from Google's guidance on strict CSP, looks like this for a nonce-based site:
Content-Security-Policy-Report-Only: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; report-to csp-endpoint
Replace {RANDOM} with a fresh value on every response, and add the same value as a nonce="{RANDOM}" attribute on each script element you want to allow, including external ones. The 'strict-dynamic' keyword lets a script you trusted by nonce load further scripts without each one needing a nonce, which keeps most third-party widgets working. The trade-off is that the policy is less strict, because anything a trusted script creates is trusted too.
If you also want to control styles, images and connections, add default-src 'self' and list the extra hosts you found in the Before you begin inventory, for example img-src 'self' https://images.example.com.
What success looks like: You have one policy string written down, with no 'unsafe-inline' and no 'unsafe-eval' in the script directive.
Step 3: Deploy it in report-only mode
Send the policy in the Content-Security-Policy-Report-Only header, not Content-Security-Policy. In this mode the browser applies the policy only to report violations; nothing is blocked. A report-only policy cannot be delivered in a <meta> tag, so this step needs a real response header.
To collect reports, declare an endpoint and refer to it from the policy:
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; report-to csp-endpoint
The report-to directive with the Reporting API is the current approach. The older report-uri directive is deprecated but still used; MDN advises sending both until report-to is supported everywhere. Even without an endpoint, the browser console prints each violation, so you can start by browsing your own site with developer tools open.
What success looks like: Browsing the site shows "would have been blocked" messages in the console, or reports arrive at your endpoint, and the pages still work normally.
Step 4: Fix the violations
Work through the reports. Each one names the page, the blocked resource and the directive that would have blocked it. Typical fixes:
- Inline event handlers such as
onclick="...": move the logic to a script file and attach it withaddEventListener(). - Inline scripts you control: add the nonce, or compute the hash. DevTools in Chromium-based browsers print the correct hash for an inline script in the console message.
eval(),new Function()and string arguments tosetTimeout(): replace them, for exampleJSON.parse()instead ofeval()for data, and pass a function tosetTimeout(). These are blocked unless you add'unsafe-eval', which weakens the policy even when nonces are present.- Third-party hosts: add the host to the right directive (for example
connect-srcfor an analytics endpoint) only if you still want that service. - Inline
styleattributes and<style>blocks: blocked oncestyle-srcordefault-srcapplies. Move them to stylesheets, or use nonces and hashes on<style>elements.
Avoid reaching for 'unsafe-inline'. It re-allows inline code, which removes most of the benefit, and browsers ignore it for a directive that also has a nonce or hash.
What success looks like: A full pass through your key templates produces no new violation reports from your own code.
Step 5: Enforce the policy
When reports have been quiet through a normal cycle of traffic (including pages you rarely visit, like checkout confirmation and account screens), change the header name to Content-Security-Policy. You can keep the report-only header alongside it to test the next revision; the enforced policy blocks and the report-only one reports, and both apply.
Switch on staging first, then on production at a quiet time, and keep the report endpoint running.
What success looks like: The enforced header appears in the response, the site works, and any block shows up as a report you can act on.
Step 6: Add the document-level protections and keep the policy current
Once script control works, add the directives that do not depend on it: frame-ancestors 'none' (or a list of the sites allowed to embed you) to prevent clickjacking, form-action 'self' if your forms only post to your own domain, and upgrade-insecure-requests if you are still cleaning up old http:// references. It does not replace HSTS, and the proper fix is still to correct the URLs, as in Fix Mixed Content: Remove http:// Resources From HTTPS Pages.
Keep the policy in version control, and add a new third-party host to it in the same change that adds the tag. The JavaScript console errors guide shows how to read what you find before each release.
What success looks like: Embedding your page in a test iframe on another origin is refused, and forms still submit.
What a CSP does not do
A CSP does not fix the vulnerability that allowed the injection, so keep escaping and sanitizing output. It does not change how search engines rank or index a page, so do not expect a visibility effect from adding one.
Crawvix's security-header check looks for a Content-Security-Policy on the home page and treats a report-only policy as not enforced. That makes it a quick way to confirm that Step 5 actually went live, though it does not judge how strict your policy is.
Final verification checklist
- The response headers on your home page and a deep page include
Content-Security-Policy(enforced), checked in browser developer tools or withcurl -I. - The script directive uses nonces or hashes, and does not contain
'unsafe-inline'or'unsafe-eval'. - A new nonce appears on every page load, not the same value each time.
object-src 'none'andbase-uriare set, andframe-ancestorsis set to the sites allowed to embed you, or'none'.- The console shows no CSP violation messages on your main templates.
- Reports arrive at your endpoint, or you have a way to read them.
- Forms, payment frames, video embeds and analytics you rely on still work after enforcing.
- The policy is stored in version control with a note on how to change it.
Common mistakes to avoid
- Enforcing on day one. Start with
Content-Security-Policy-Report-Onlyand review reports first. Switch only when they are clean. - Reusing one nonce. A fixed nonce is no protection. Generate a new unpredictable value per response, and never cache an HTML page that contains a nonce.
- Adding
'unsafe-inline'to make errors go away. It re-allows inline script and defeats much of the point. Refactor handlers or use nonces and hashes. - Forgetting quotes.
'self'and'none'need single quotes;https:and hostnames do not. - Expecting
default-srcto cover everything.base-uri,form-action,frame-ancestorsandsandboxare not covered by the fallback. - Putting nonces on every script tag automatically. Add them through your templates for scripts you wrote. If injected content also receives a nonce, an attacker's script will run.
- Relying on the meta tag for reporting. Report-only and some directives need the real header.
Frequently asked questions
Will a content security policy improve my search rankings?
No. CSP is a security control, and this guide makes no ranking claim. Its value is reducing the damage from injected scripts.
Should I use nonces or hashes?
Use nonces when the server renders each page per request, because each response can carry a fresh value. Use hashes for static HTML, where the content is fixed. A hash must be recalculated whenever the script changes.
Can I set a CSP in a meta tag?
Yes, but with limits. MDN says meta delivery does not support all CSP features, and a report-only policy cannot be delivered that way. A response header is the better choice.
What is the difference between Content-Security-Policy and Content-Security-Policy-Report-Only?
The first blocks resources that break the policy. The second only reports them. You can send both at once: the enforced policy blocks, and the report-only one lets you test a stricter revision.
My Cloudflare or analytics script stopped working. What now?
Check the console and your reports to see which directive blocked it, then allow the exact host in that directive or load it through a trusted, nonced script. Cloudflare's own documentation notes that Rocket Loader and Bot Fight Mode's JavaScript detections need CSP adjustments, so see Cloudflare SEO Settings That Help or Hurt Crawling.
Related guides
- Add HTTP Security Headers: CSP, nosniff, Referrer-Policy and More
- Fix Mixed Content: Remove http:// Resources From HTTPS Pages
- Find and Fix JavaScript Console Errors on Page Load
- HSTS Setup Guide: Roll Out Strict-Transport-Security Safely
- Cloudflare SEO Settings That Help or Hurt Crawling
Official references
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy
- https://web.dev/articles/strict-csp
- https://developers.cloudflare.com/speed/optimization/content/rocket-loader/
- https://developers.cloudflare.com/bots/get-started/bot-fight-mode/
Next step: See how Crawvix checks a website and capture a baseline once scanning opens. This guide provides technical information, not a guarantee of rankings, traffic or index inclusion.
Crawvix Editorial Team
· Published October 29, 2026 · Last updated October 29, 2026 · 9 min read