How to Fix Missing Content-Security-Policy Header (Step-by-Step)
Add a Content-Security-Policy header without breaking your site: start in report-only mode, then enforce. Copy-paste steps for Cloudflare, Nginx and Apache.
Your site is missing a Content-Security-Policy header. Here is what CSP does, why it matters and exactly how to add it on Cloudflare, Nginx and Apache - without breaking your site in the process.
What is covered
- What is CSP and what does it do?
- Why missing CSP is a real problem
- How Guardr scores a missing CSP header
- Before you add CSP - start in report-only mode
- How to add CSP on Cloudflare
- How to add CSP on Nginx
- How to add CSP on Apache
- Understanding the key CSP directives
- Common mistakes when deploying CSP
- How to verify your CSP is working
What is CSP and what does it do?
Content-Security-Policy (CSP) is a response header that tells the browser which sources of script, style, font, image and other content the page is allowed to load. Anything not on that list is blocked before it runs.
The case CSP is built for is cross-site scripting (XSS). XSS happens when someone manages to inject JavaScript into your page - through a comment field, a URL parameter, a compromised third-party script or any other input that ends up reflected in the HTML. Without CSP, an injected script runs with the same access as your own code. It can read session cookies, capture form input, redirect users or quietly make requests to external servers.
With CSP in place, that same injected script hits a wall. If the script is not loaded from an origin your policy explicitly allows, the browser refuses to run it. The mechanism is that simple: a list of allowed sources, applied by the browser, on every load.
Beyond XSS, CSP covers two related misconfigurations:
Clickjacking. The frame-ancestors directive controls which domains can embed your page in an <iframe>. It is the modern replacement for the X-Frame-Options header and it is more flexible - you can allow specific domains rather than only same-origin. Our guide to fixing a missing X-Frame-Options header covers the older header in full.
Mixed content. The upgrade-insecure-requests directive tells the browser to request HTTP resources over HTTPS instead, so a page served over HTTPS does not end up pulling part of itself over an unencrypted connection.
Why missing CSP is a real problem
A missing CSP header does not mean your site is broken. It means there is no browser-enforced rule standing between a successful XSS payload and your users.
Consider what happens on a site without CSP once someone finds an XSS entry point - a stored comment, a URL parameter reflected in the page, a third-party script that gets compromised. The browser has no policy to apply. The injected script loads. It reads document.cookie and sends the session token elsewhere. It silently logs keystrokes on the login form. It injects a fake password-reset prompt. The user has no idea any of this happened.
CSP does not stop the injection from reaching the HTML in the first place - that is the job of output encoding and input sanitization on the server side. What CSP does is contain the blast radius. Even if a script makes it into your page, a well-configured policy can stop it from running or from loading anything external.
A missing CSP is also a signal that the site’s header configuration has not been fully thought through. For agencies handing over client sites and for developers running sites that handle user data, it is the kind of gap that comes up in any serious review.
How Guardr scores a missing CSP header
Guardr checks for Content-Security-Policy as part of the security headers category, which accounts for 28% of your total score. The other headers in this category are X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Permissions-Policy. HSTS sits in the TLS/SSL category, also 28% of the total - see our guide to fixing a missing HSTS header for that one.
A missing CSP is flagged as a high severity finding - the highest severity in the headers category.
Guardr’s evaluation is context-aware. It does not just check whether the header exists. As the methodology page notes: “a site with CSP deployed but misconfigured (e.g. unsafe-inline with no nonce or hash) will lose partial points rather than receiving full credit.” Specifically, Guardr checks:
- Whether the policy is enforced or report-only (report-only is flagged as medium severity - you are watching but not blocking)
- Whether
default-srcandscript-srcare defined - Whether
frame-ancestorsis present (or whetherX-Frame-Optionscovers clickjacking instead) - Whether
'unsafe-inline'appears inscript-srcwithout nonces or hashes, which weakens the policy - Whether
'unsafe-eval'is present, which allowseval()and weakens the policy further
The grade reflects this nuance. A completely absent CSP pulls the headers part of your score toward zero for that check. A report-only CSP scores better but is still flagged. A CSP with 'unsafe-inline' and no nonces lands somewhere in the middle. The goal is to give you an accurate picture of where you actually stand - not a binary pass or fail.
What you get back is an A to F health grade. Every finding behind it comes with a fix. In the Guardr dashboard those fixes are written for the platform you are on: Cloudflare, Nginx or Apache - the same three covered below.
Before you add CSP - start in report-only mode
This is the most important thing to understand about CSP: a misconfigured enforced policy will break your site for real users. If your policy does not allow your analytics script, your payment processor widget or your font CDN, those resources silently fail to load for everyone.
The fix is to always start with Content-Security-Policy-Report-Only before switching to Content-Security-Policy. The report-only header applies your policy in monitoring mode - the browser enforces nothing, but it logs every resource that would have been blocked to the browser console and optionally to a reporting endpoint.
This lets you see exactly what your site loads, discover gaps in your policy and fix them before any real enforcement happens.
The workflow is:
- Deploy the report-only header with a starting policy
- Browse your site thoroughly - homepage, login, checkout, any page that loads third-party resources
- Open DevTools → Console and look for CSP violation messages
- Update your policy to include the missing allowed sources
- Repeat until you see zero violations
- Switch
Content-Security-Policy-Report-OnlytoContent-Security-Policy
Do not skip this step. The more third-party scripts, fonts and CDN resources your site loads, the more violations you will find - and they all need to be accounted for before enforcement.
How to add CSP on Cloudflare
Option 1: _headers file (Cloudflare Pages)
If you are deploying via Cloudflare Pages, create or edit a _headers file in your project’s public output directory:
/*
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'self'
Once you have audited violations and tuned your policy, switch to enforcement:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.analytics.com; frame-ancestors 'self'
Replace the allowed origins with the actual domains your site loads from. Deploy the file and Cloudflare Pages serves the header on every response.
Option 2: Cloudflare Transform Rules (Cloudflare proxy)
If you are using Cloudflare as a proxy in front of your origin server (not Pages), use Transform Rules to inject the header:
Go to Cloudflare Dashboard → your site → Rules → Transform Rules → Modify Response Header. Create a rule that adds the Content-Security-Policy header with your policy value. Set the rule to match all incoming requests (true).
This approach is useful when you cannot modify your origin server config directly.
How to add CSP on Nginx
Add the header inside your server block for your HTTPS configuration. Start with report-only:
server {
listen 443 ssl http2;
server_name example.com;
# Step 1: report-only - audit what your site loads
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'self'" always;
# ... rest of your config
}
After auditing violations in DevTools, switch to enforced mode with your tuned policy:
server {
listen 443 ssl http2;
server_name example.com;
# Step 2: enforce after testing
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'self'" always;
# ... rest of your config
}
The always keyword ensures the header is sent on error responses (4xx and 5xx) as well as successful ones. Without it, Nginx only adds headers to 2xx responses.
Test and reload after changes:
nginx -t
systemctl reload nginx
How to add CSP on Apache
Enable the headers module if it is not already active, then add the header in your VirtualHost configuration or .htaccess file.
VirtualHost configuration:
# a2enmod headers (if not already enabled)
<VirtualHost *:443>
ServerName example.com
# Step 1: report-only to audit
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'self'"
# ... rest of your config
</VirtualHost>
After testing, switch to enforcement:
<VirtualHost *:443>
ServerName example.com
# Step 2: enforce after testing
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'self'"
</VirtualHost>
.htaccess (shared hosting):
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'self'"
</IfModule>
Restart Apache after changes:
systemctl restart apache2
Understanding the key CSP directives
A realistic starting policy looks like this:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'self'
Here is what each directive does:
default-src 'self' - the catch-all fallback. Any resource type not covered by a specific directive falls back to this rule. 'self' means only your own origin is allowed. This is the most important directive to include.
script-src 'self' - controls which JavaScript is allowed to run. This is the directive that actually stops XSS. 'self' allows scripts only from your own origin. To allow external scripts, add their origin: script-src 'self' https://cdn.jsdelivr.net. To allow inline scripts without weakening the policy, use nonces ('nonce-{random}') or hashes ('sha256-{hash}') instead of 'unsafe-inline'.
style-src 'self' 'unsafe-inline' - controls stylesheets. 'unsafe-inline' is commonly needed here because many frameworks and tools inject inline styles. The risk from inline styles is far lower than from inline scripts, so this is an acceptable tradeoff in most cases.
img-src 'self' data: https: - allows images from your origin, data URIs (common for inline SVGs and base64 images) and any HTTPS source. The https: scheme source is broader than ideal but practical for sites pulling images from many external domains.
font-src 'self' https: - controls web fonts. If you are using Google Fonts, you need https://fonts.gstatic.com here (or the broader https: to cover any font CDN).
connect-src 'self' - controls fetch, XHR, WebSocket and EventSource connections. If your frontend makes API calls to an external domain, add it here: connect-src 'self' https://api.example.com.
frame-ancestors 'self' - controls which domains can embed your site in an <iframe>. This is the modern replacement for X-Frame-Options: SAMEORIGIN. Use frame-ancestors 'none' if your site should never be framed.
Common mistakes when deploying CSP
Enforcing before auditing. The number one mistake. Deploy report-only first, browse your site, fix the policy until there are zero violations, then enforce. Skipping to enforcement will break real users.
Using 'unsafe-inline' in script-src. This undoes most of what CSP does about XSS. An injected script qualifies as “inline” and runs freely. If your codebase uses inline scripts, migrate them to external files or use nonces. If a third-party tag manager injects inline scripts, ask whether nonces can be passed to it.
Using 'unsafe-eval' in script-src. This allows eval(), new Function() and similar dynamic code execution methods. Some older libraries require it but modern ones generally do not. Check whether your stack actually needs it before including it.
Setting script-src * (wildcard). Allowing scripts from any origin means a compromised CDN can inject code. So can anyone who manages to get an external URL referenced on your page. Enumerate your actual script sources explicitly.
Forgetting about subdomains. 'self' matches your exact origin - https://example.com. It does not automatically include https://static.example.com or https://api.example.com. Add subdomains you need explicitly.
Setting CSP only on the HTML document. Ideally CSP applies to every response from your origin - not just the HTML page. Configure it at the server or CDN level so it is served on all routes.
Copy-pasting a policy without tailoring it. Generic example policies often include broad sources like https: for scripts. These pass the basic check but do very little in practice. Take the time to enumerate the specific origins your site actually uses.
How to verify your CSP is working
Using browser DevTools:
Open your site in Chrome or Firefox, press F12 and go to the Network tab. Click the main document request and check Response Headers for Content-Security-Policy or Content-Security-Policy-Report-Only. If the header is present, your policy is being served.
To see violations while in report-only mode, open the Console tab. Any resource that your policy would block generates a message like:
Refused to load the script 'https://external.example.com/lib.js' because it violates the following Content Security Policy directive: "script-src 'self'"
Each violation tells you which resource was blocked and which directive it violated. Add that source to your policy and reload until the console is clean.
Using curl:
curl -I https://yoursite.com
Look for content-security-policy in the response headers. You should see your full policy string.
Using Guardr:
Guardr reads the header as enforced, report-only or absent and reports each state as its own finding: cleared, medium severity or high severity.
Just shipped the fix? Run the free Guardr check on your site - it reads the header back in a few seconds and needs no account. If you look after more than one site, the same page shows what it costs to keep watch on all of them.
If SecurityHeaders.com is your usual tool, our comparison of Guardr and SecurityHeaders.com sets out where the two differ.
CSP is one of the more involved security headers to get right - there is no single value you paste in everywhere, because every site loads different resources. But the report-only approach makes the deployment safe and systematic. Browse your site, fix what the console shows you, enforce when the violations stop.
The result is a real limit on what an injected script can do, fewer clickjacking and mixed-content gaps - and a headers grade that reflects the work you have put in.
See the full scoring breakdown in the Guardr methodology.