How to Fix Missing Content-Security-Policy Header (Step-by-Step)

Your site is missing a Content-Security-Policy header. Learn what CSP does, why it matters and exactly how to add it on Cloudflare, Nginx and Apache.

cspsecurity-headersweb-securityguidecontent-security-policy

Your site is missing a Content-Security-Policy header. Here’s 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’s covered


What is CSP and what does it protect against?

Content-Security-Policy (CSP) is a response header that tells browsers which scripts, stylesheets, fonts, images and other resources are allowed to load on your page. Everything not on the list gets blocked before it executes.

The primary threat CSP is designed to limit is cross-site scripting (XSS). XSS happens when an attacker 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 full access to your page: it can steal session cookies, capture form input, redirect users or silently make requests to external servers.

With CSP in place, that same injected script hits a wall. If the script isn’t loaded from an origin your policy explicitly allows, the browser refuses to execute it.

Beyond XSS, CSP addresses two related misconfigurations:

Clickjacking. The frame-ancestors directive controls which domains can embed your page in an <iframe>. It’s the modern replacement for the X-Frame-Options header and is more flexible — you can allow specific domains rather than just same-origin.

Mixed content. The upgrade-insecure-requests directive tells browsers to automatically upgrade HTTP resource requests to HTTPS, which prevents attackers from injecting content through unencrypted resource loads on an otherwise HTTPS page.


Why missing CSP is a real problem

A missing CSP header doesn’t mean your site is broken. It means there’s no browser-enforced rule stopping a successful XSS payload from doing damage.

Consider what happens on a site without CSP after an attacker 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 enforce. The injected script loads. It reads document.cookie and exfiltrates the session token. 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 doesn’t prevent the XSS injection from reaching the HTML — that’s the job of output encoding and input sanitization on the server side. What CSP does is contain the blast radius. Even if an attacker gets a script into your page, a well-configured CSP policy can prevent it from executing or loading external resources.

Beyond XSS, a missing CSP is also a signal that the site’s security posture hasn’t been fully thought through. For agencies handing over client sites, and for developers managing sites that handle user data, it’s a gap that comes up in any serious security 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 security score. The other headers in this category are X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Permissions-Policy. HSTS lives in the TLS/SSL category — also 28% of the total score.

A missing CSP is flagged as a high severity finding — the highest severity in the headers category.

Guardr’s evaluation is context-aware — it doesn’t 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’re monitoring but not blocking)
  • Whether default-src and script-src are defined
  • Whether frame-ancestors is present (or whether X-Frame-Options covers clickjacking protection instead)
  • Whether 'unsafe-inline' appears in script-src without nonces or hashes (weakens protection)
  • Whether 'unsafe-eval' is present (allows eval() — further weakens protection)

The scoring reflects this nuance. A completely absent CSP pulls your headers score toward zero for that check. A report-only CSP scores better but is still flagged. A CSP with 'unsafe-inline' and no nonces scores somewhere in the middle. The goal is to give you an accurate picture of where you actually stand — not a binary pass/fail.

In the Guardr dashboard, each finding includes platform-specific remediation instructions for Cloudflare, Nginx and Apache — the same three platforms 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 doesn’t 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:

  1. Deploy the report-only header with a starting policy
  2. Browse your site thoroughly — homepage, login, checkout, any page that loads third-party resources
  3. Open DevTools → Console and look for CSP violation messages
  4. Update your policy to include the missing allowed sources
  5. Repeat until you see zero violations
  6. Switch Content-Security-Policy-Report-Only to Content-Security-Policy

Don’t skip this step. The more third-party scripts, fonts and CDN resources your site loads, the more violations you’ll 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’re 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’ve 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’re 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 can’t 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 isn’t 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’s 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 execute. 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 security risk from inline styles is significantly 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’re 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 negates most of CSP’s XSS protection. 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 don’t. Check whether your stack actually needs it before including it.

Setting script-src * (wildcard). Allowing scripts from any origin means any compromised CDN or an attacker who can reference any external URL can inject code. 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’s 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 provide weak protection. 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:

Scan your site for free at guardr.io — Guardr checks your CSP as part of the security headers category and tells you exactly what’s present, what’s missing and what’s weak. If your CSP is enforced and well-configured, the finding is cleared. If you’re in report-only mode, you’ll see a medium severity finding reminding you to enforce. If CSP is absent entirely, you’ll see the high severity finding with the fix instructions for your platform.


CSP is one of the more complex security headers to get right — there’s 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 meaningful layer of protection against XSS, clickjacking and mixed content misconfigurations — and a security headers score that reflects the work you’ve put in.

Scan your site and see your current CSP status →


See the full scoring breakdown in the Guardr methodology.

Check your website's security score

Free scan — no signup required.

Scan your site →