/ โ† Security Audit Fixes / CSP report-only
In this guide
  1. What Content-Security-Policy-Report-Only does
  2. Where a Content Security Policy is set
  3. Why default-src 'self' is not working
  4. How to receive and read violation reports
  5. Adding the report-only header on common servers
  6. A worked example: from report-only to enforced
  7. The fix, step by step
  8. Edge cases worth knowing
  9. Keeping the policy healthy
  10. Questions and answers

What Content-Security-Policy-Report-Only does

A Content Security Policy can be sent in two forms. Content-Security-Policy enforces the rules: the browser refuses to load anything the policy does not allow. Content-Security-Policy-Report-Only takes exactly the same directives, but the browser blocks nothing. It only notes each resource that would have been refused, prints a warning in the Console and, if you give it an address, sends a report.

Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; report-uri /csp-reports

That makes report-only the safe way to introduce or tighten a policy on a live site. You see the full list of scripts, styles, fonts, frames and connections your pages actually use, including ones added by plugins and tag managers that nobody remembers, before a single visitor sees a broken page.

You can send both headers at once. A common pattern is to enforce a loose policy you know works while trialling a stricter one in report-only mode. The browser applies them independently.

๐Ÿ’ก Report-only mode protects nothing on its own. It is a measuring tool. Plan from the start to switch to the enforcing header once the reports go quiet.

Where a Content Security Policy is set

One of the most common questions is simply where the policy comes from, because it can be added at several layers and the browser cannot tell you which one sent it. Work through these in order until you find it.

LayerWhere to lookTypical example
CDN or proxyDashboard rules for response headersCloudflare Transform Rules, a Worker, a load balancer policy
Web serverSite configuration and include filesadd_header in nginx, Header set in Apache or .htaccess
Control panelPer-domain web server settingsPlesk Apache and nginx Settings, additional directives
ApplicationMiddleware, framework config, filtersSpring Security, Express helmet, a Laravel middleware
CMS pluginSecurity or header pluginsA WordPress security plugin with a headers screen
Page markupThe <head> of the HTML<meta http-equiv="Content-Security-Policy">

To see what is actually delivered, request the page and look at both the headers and the HTML:

curl -sI https://example.com/ | grep -i content-security
curl -s https://example.com/ | grep -io '<meta[^>]*content-security[^>]*>'

If the header appears on the CDN address but not when you request the origin server directly, the CDN is adding it. If it appears on every site on the server, it is in a global configuration file rather than the site's own. On a Linux server you can search configuration for it:

sudo grep -ril "content-security-policy" /etc/nginx /etc/apache2 /etc/httpd /var/www/vhosts/*/conf 2>/dev/null

The platform guides for nginx, Cloudflare and WordPress show where each one stores headers.

Why default-src 'self' is not working

People often add default-src 'self', see the site break or see nothing change at all, and conclude the policy is not working. It nearly always is working; it is just doing something different from what they expected. These are the usual reasons.

What you seeWhy it happensWhat to do
Inline scripts and styles stop working'self' covers files from your own origin, not code written inside the page or in onclick attributesMove code to files, or use nonces or hashes
Google Fonts, analytics or embeds vanishEvery other domain is blocked, including your own CDN and subdomainsAdd each host to the right directive, such as font-src
www and bare domain behave differently'self' means the exact scheme, host and port of the pageList the other host, or load everything from one host
Your own files are blocked tooself written without single quotes is read as a host called selfWrite 'self' with the quotes
Nothing is blocked, header presentIt was sent as -Report-Only, or it is a meta tag placed after the scriptsCheck the header name and position
The header is not therenginx inheritance, a missing always flag, a CDN stripping it, or a cached responseTest the origin and the CDN separately with curl
Framing protection not workingdefault-src does not cover frame-ancestors, base-uri or form-actionSet those three directives explicitly

A further trap is two policies. If a plugin sends one header and your server sends another, both are enforced, and a resource has to be allowed by both. Adding a host to one of them then appears to make no difference.

โš ๏ธ Adding 'unsafe-inline' to script-src to make inline code work removes most of the protection a CSP gives against injected scripts. Use it for styles only if you must, and treat it as temporary.

How to receive and read violation reports

The Console shows violations in your own browser. To see what real visitors' browsers hit, on pages and devices you never test, add a reporting directive. There are two, and it is sensible to send both for now.

Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; report-uri https://example.com/csp-reports; report-to csp

report-uri is the older directive and is marked deprecated, but it is still widely supported. report-to names an endpoint from the Reporting-Endpoints header. Browsers that support report-to use it and ignore report-uri; others fall back to report-uri. Reporting directives are ignored in a meta tag, so this needs a real header.

What a report looks like

A report-uri report arrives as a POST with the content type application/csp-report:

{
  "csp-report": {
    "document-uri": "https://example.com/menu",
    "violated-directive": "font-src",
    "effective-directive": "font-src",
    "blocked-uri": "https://fonts.gstatic.com",
    "disposition": "report",
    "original-policy": "default-src 'self'; object-src 'none'; report-uri /csp-reports"
  }
}

Reports sent through report-to use the content type application/reports+json, arrive as an array, and use camel-case field names such as documentURL, blockedURL and effectiveDirective. A small endpoint only has to accept the POST, write the body to a log and return 204.

Reading them

Adding the report-only header on common servers

Use the same header name and policy whichever server you have. Only the syntax changes.

nginx

add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; report-uri /csp-reports" always;

The always flag adds the header to error responses as well. Remember that any add_header in a location block replaces those set higher up.

Apache or .htaccess

<IfModule mod_headers.c>
  Header always set Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; report-uri /csp-reports"
</IfModule>

Plesk

On a Plesk server, open the domain, go to Apache and nginx Settings, and add the nginx line to the additional nginx directives, or the Apache line to the additional HTTP and HTTPS directives. Use one, not both. The Plesk security headers guide shows the screens.

Cloudflare

Create a Response Header Transform Rule that sets Content-Security-Policy-Report-Only to your policy. Check that your origin is not sending its own policy too.

After any change, run curl -sI again and confirm the header appears once, with the exact name and policy you intended.

A worked example: from report-only to enforced

Example Bakery adds default-src 'self'; object-src 'none'; base-uri 'self' as report-only on its WordPress site, with a report-uri pointing to a small logging script. After three days the log holds about 2,400 reports, which group into six lines:

DirectiveBlockedDecision
style-srcfonts.googleapis.comAllow in style-src
font-srcfonts.gstatic.comAllow in font-src
script-srcwww.googletagmanager.comAllow in script-src, and its collection host in connect-src
script-srcinlineTheme's inline settings script: add a nonce through the theme
frame-srcwww.youtube-nocookie.comAllow in frame-src
script-srcchrome-extensionIgnore

They add the five decisions, leave it in report-only mode for another week, and see only extension noise. They then rename the header to Content-Security-Policy, keep report-uri, and watch the log for the first few days after the change.

The fix, step by step

Step 1

Find where any current policy is set

Run curl -sI on the live site and on the origin server if you use a CDN, and search the HTML for a CSP meta tag. Check web server configuration, control panel directives, CDN header rules, application middleware and CMS plugins. Write down every place a policy is set. You are ready to move on when you know exactly which layer sends each policy and you have decided which single layer will own it from now on.

Step 2

Remove duplicate policies

Turn off every copy except the one you chose, for example a security plugin's header screen when the server already sends a policy. Purge any page cache and CDN cache. Run curl again on several URLs, including an error page. You should see no policy at all, or only the one you intend to keep, so that the report-only trial you add next is measuring a clean starting point.

Step 3

Add a report-only header with a starter policy

Send Content-Security-Policy-Report-Only with default-src 'self'; object-src 'none'; base-uri 'self' plus img-src 'self' data: if you use data images. Add it on your chosen layer: nginx, Apache, Plesk, Cloudflare or the application. Make sure 'self' is written with single quotes. Confirm with curl that the header is present once and that the site looks and works exactly as before, because a report-only header should never change what visitors see.

Step 4

Set up a reporting endpoint

Create a small script at a path such as /csp-reports that accepts a POST, writes the body to a log file and returns 204. Add report-uri pointing to it, and optionally report-to with a matching Reporting-Endpoints header. Test it by loading a page that uses an external font: within seconds a report naming font-src should appear in the log. If nothing arrives, check the endpoint path, the response code and any firewall rule blocking POST requests.

Step 5

Collect violations for a few days

Leave the trial running long enough to cover normal traffic, ideally a week, including any pages people visit rarely such as checkout, account and contact forms. Browse them yourself with the Console open as well. Check the endpoint is not slowing pages and that the log is not growing faster than you can store. You are done when new report types stop appearing.

Step 6

Group and decide on each source

Group the reports by effective directive and blocked address. For each group decide whether to allow it, replace it, or ignore it. Add trusted hosts to the narrowest directive that fits, for example font-src for font files. For inline scripts, add nonces or hashes or move the code into files. Ignore extension addresses. Update the report-only policy with your decisions and redeploy it.

Step 7

Confirm the trial goes quiet

Watch the reports for several more days after updating the policy. The only remaining entries should be browser extension noise and sources you deliberately chose to block. If a real feature still appears, fix that source and wait again. Test the main journeys, such as forms, logins and payments, while watching the Console, so that you see any refusals the reports have not yet caught.

Step 8

Switch to the enforcing header

Rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy, keeping the same directives and the reporting directive. Purge caches, then run curl to confirm the new header name is returned on every template. Test the main journeys straight away. If anything breaks, revert to report-only within minutes rather than adding 'unsafe-inline' or wildcards to the enforced policy, then fix the source in report-only mode and try again.

Edge cases worth knowing

Keeping the policy healthy

Once enforced, keep the reporting directive. It is the only way you will hear that a new marketing tag or a plugin update has started loading from a host the policy does not allow.

The framework CSP guide covers Spring Boot, Tomcat and Angular, and the missing security headers guide covers the other headers scanners expect.

Questions and answers

1. What is Content-Security-Policy-Report-Only?

Content-Security-Policy-Report-Only is a response header that tests a Content Security Policy without enforcing it. The browser checks every resource against the policy, prints a warning in the Console for anything that would be refused and sends a report if you include a reporting directive, but it does not block anything. That makes it the safe way to roll out or tighten a policy on a live site, because you see exactly what would break before any visitor is affected by it.

2. Why is my default-src self policy not working?

Usually default-src self is working, but it blocks more or less than you expected. It does not allow inline scripts or styles, other domains, subdomains or your CDN, which is why sites often break. If your own files are blocked, check that self is in single quotes; if nothing is blocked, check the header is not the report-only version, and that a CDN, cache or nginx location block is not removing it. Also remember that default-src does not cover frame-ancestors, base-uri or form-action.

3. Where is the Content Security Policy set?

A Content Security Policy is set in a response header or, less usefully, in a meta tag in the page. The header can come from a CDN rule, the web server configuration, a control panel setting such as Plesk directives, the application code or a CMS plugin. To find it, compare curl output for the CDN address and the origin server, search the server configuration files, and check plugins. If the header disappears when you request the origin directly, the CDN is the layer adding it.

4. Does report-only mode protect my site?

No, report-only mode does not protect your site, because nothing is blocked. An injected script still runs exactly as it would without any policy. It is a measuring tool for building a policy you can enforce. Run it until the reports show only sources you have decided about, then switch to the enforcing header. You can keep a report-only header alongside the enforcing one afterwards, to trial stricter rules without risking visitors.

5. Can I use report-only in a meta tag?

No, browsers ignore Content-Security-Policy-Report-Only when it is placed in a meta tag. Report-only mode only works as a real HTTP response header, and the reporting directives report-uri and report-to are also ignored in meta tags. If you cannot change headers on your hosting, look for a control panel option, a CDN header rule or an application setting that can add them, because a meta tag cannot give you a safe trial run with reports.

6. Should I use report-uri or report-to?

Send both for now, because browser support for report-to is not yet universal and report-uri, although deprecated, is still widely supported. Browsers that understand report-to use it and ignore report-uri, while others fall back to report-uri. The report-to directive needs a separate Reporting-Endpoints header that names the endpoint. The two formats differ slightly, so your collecting script should accept both the application/csp-report and the application/reports+json content types and log them in one place.

7. How long should I run CSP in report-only mode?

Run CSP in report-only mode until new kinds of report stop appearing, which on most small sites takes one to two weeks. You need enough traffic to cover rarely visited pages, such as checkout, account and contact forms, plus the different devices and browsers your visitors use. A busy site may see every source within a day or two. Restart the trial for a few days whenever you add a major plugin, theme or third-party tool, so new sources are caught before they are blocked.

Check which security headers your site sends

Run the security audit on aiwebpageseo.com to see whether your pages send an enforced or report-only Content Security Policy, whether it is duplicated, and which other headers are missing.

Open Security Audit Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All Security Audit Fixes  ·  CSP in Spring Boot, Tomcat and Angular  ·  Fix missing security headers  ·  Security headers on Cloudflare  ·  Security headers on Plesk  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.