HostleloBlogExplore hosting

Add a Content-Security-Policy in Report-Only Mode, Safely

Test a CSP header on Apache without breaking your site: collect violation reports first, fix your policy, enforce later, and keep a one-step rollback ready.

On this page

The short answer

Send your policy first as the Content-Security-Policy-Report-Only header from .htaccess. The browser blocks nothing and only reports what would have broken, such as inline scripts, fonts, analytics and embedded videos. Fix the policy until the reports are quiet, then switch the header name to Content-Security-Policy, with your .htaccess backup ready for rollback.

You can add a Content Security Policy to an Apache site without breaking it by sending it first as Content-Security-Policy-Report-Only. In that mode the browser blocks nothing. It only tells you what it would have blocked, so you can fix your policy before you switch it on for real.

Here is the part that surprises most site owners: a single line like default-src 'self' quietly blocks every inline script on your pages, your analytics snippet, your web fonts from another domain, and that embedded video on the homepage. Report-only mode lets you find that out from your browser instead of from customers.

By the end of this guide you will have a report-only policy running from .htaccess, you will know how to read what it reports, and you will have a safe path to enforce it later, with a one-step rollback.

What a Content Security Policy actually is

A Content Security Policy (CSP) is an HTTP response header that tells the browser which sources it may load scripts, styles, images, fonts and other resources from. If something on the page tries to load from a source that is not on the list, the browser refuses.

Think of it as a guest list at the door of a party. The bouncer (the browser) checks every arrival against the list. In report-only mode, the bouncer lets everyone in but writes down the name of each person who was not on the list. Next morning you decide who should have been invited.

Why this matters: inline JavaScript is one of the most common ways attackers run their code on a site (cross-site scripting, or XSS). MDN says one of the most basic goals of a CSP is to stop the uncontrolled use of inline JavaScript. A good policy is a second wall if a plugin ever lets bad code slip in.

Heads up: MDN is clear that a CSP is not a replacement for cleaning user input. It is extra protection on top of updated software and safe forms.

Before you start

  • Access: a way to edit the .htaccess file in your site root, through your hosting file manager, FTP, or SSH.
  • Server: Apache (or a server that reads Apache .htaccess rules). This guide uses Apache syntax only.
  • Permission: your host must allow Header lines in .htaccess. Apache documents the Header directive with Override FileInfo, which means the server config needs AllowOverride FileInfo (or broader) for your folder.
  • Tools: a terminal with curl, and a desktop browser with developer tools.
  • Backup: a copy of your current .htaccess (Step 1 below).
  • Time: about 15 minutes to set up, then a few days of watching reports.
  • Risk level: low for report-only mode, because nothing is blocked. The real risk is a typo in .htaccess, which can take the whole site down with a server error. The backup fixes that in seconds.

Quick tip: if your site is not on HTTPS yet, sort that out first with our guide on how to redirect HTTP to HTTPS using .htaccess. Report endpoints must be HTTPS anyway, and MDN notes that non-secure endpoints are ignored.

How report-only mode works

There are two CSP headers. The difference is what the browser does when a rule is broken.

  • Content-Security-Policy enforces the policy. Blocked things stay blocked.
  • Content-Security-Policy-Report-Only enforces nothing. The browser reports violations and loads the page as normal.

The W3C CSP Level 3 draft (Working Draft dated 16 September 2026) describes report-only as a way to experiment with a policy by monitoring its effect instead of enforcing it.

If both headers are present on the same response, the browser honors both. The enforced one blocks, the report-only one only reports.

One more rule: a report-only policy cannot be set with an HTML meta tag. It must be a real HTTP header, so we use .htaccess.

The directives you will use

Each part of a policy is called a directive. Here are the ones we use, as MDN defines them.

  • default-src: the fallback for the other fetch directives. If you do not set a more specific one, this applies.
  • script-src: valid sources for JavaScript (and WebAssembly).
  • style-src: valid sources for stylesheets.
  • img-src: valid sources for images and favicons.
  • font-src: valid sources for fonts loaded with @font-face.
  • connect-src: limits the URLs that scripts can load, such as the background calls an analytics or chat widget makes.
  • frame-src: valid sources for content inside iframe elements, such as embedded videos or maps.
  • report-to and report-uri: where the browser sends violation reports.

The keyword 'self' means "same origin as the page itself". So default-src 'self' loads resources only from your own site, unless a more specific directive says otherwise.

Set up a report-only CSP in .htaccess

Made-up example: Sara runs a small shop at example.com. Her theme has an inline script for the cart menu. She loads fonts from fonts.example.com, analytics from analytics.example.com, product photos from a CDN at cdn.example.com, and a demo video embedded from video.example.com. Swap in your own domains when you copy her policy.

Step 1: Back up the .htaccess file

In your hosting file manager, open your site root (often public_html), find .htaccess, and copy it to a file named .htaccess.bak-csp. If you do not see it, turn on showing hidden files (dotfiles) in the file manager settings.

If you have SSH access, this command makes the same copy and keeps the original date and permissions.

cd ~/public_html && cp -p .htaccess .htaccess.bak-csp && ls -la .htaccess*

You should see both .htaccess and .htaccess.bak-csp in the list, with the same size.

Also save the current headers so you can compare later. This command prints the response headers for your homepage.

curl -sI https://example.com/

Keep that output in a text file. It is your "before" picture.

Step 2: Start with the simplest report-only policy

Start small. This first version sends no reports anywhere. The browser only shows violations in its developer tools.

Open .htaccess in the file manager editor and add this block at the very top of the file. It sends a report-only header that allows only your own origin.

<IfModule mod_headers.c>
  Header always set Content-Security-Policy-Report-Only "default-src 'self'"
</IfModule>

What each part does:

  • IfModule mod_headers.c runs the line only if Apache's headers module (headers_module, source file mod_headers.c) is loaded. If it is missing, Apache skips the block instead of failing.
  • Header always set adds the header. Apache's docs say always headers are added even on error responses, and set replaces any earlier header with the same name, so you do not end up with duplicates.
  • The policy sits inside double quotes, and 'self' keeps its single quotes.

Save the file. Then reload any page of your site in the browser. It should look and work exactly as before, because report-only mode blocks nothing.

Heads up: if your site now shows "500 Internal Server Error", restore the backup (see the rollback section) and check the troubleshooting list.

Step 3: Read the violations in your browser

Now we find out what default-src 'self' would have broken.

Open your homepage in Chrome, right click the page, and choose Inspect to open the developer tools (DevTools). Then look in two places:

  • Console tab: look here too for messages that mention Content Security Policy, the blocked address and the directive.
  • Issues panel (Chrome): click the Open Issues button next to Settings in the top right of the DevTools bar, or open More tools > Issues. Chrome's docs list "Content Security Policy violations" as one of the issue types it reports.

Write down every source that appears. For Sara, the list looked like this:

  • The inline cart script in her theme.
  • The font files and font stylesheet from fonts.example.com.
  • Product images from cdn.example.com.
  • The analytics script and its background calls to analytics.example.com.
  • The embedded video from video.example.com.

That is the realistic risk in one list. Every item on it would have been broken if she had gone straight to enforce mode.

Quick tip: do not test only the homepage. Visit checkout, contact, login, posts with embeds, and any page with a map or chat widget.

Step 4: Build a fuller policy

Now turn your list into rules, one directive per type of resource. This is Sara's made-up example policy, split across lines so you can read it. Do not paste this version.

default-src 'self';
script-src 'self' https://analytics.example.com;
style-src 'self' https://fonts.example.com;
font-src 'self' https://fonts.example.com;
img-src 'self' https://cdn.example.com;
connect-src 'self' https://analytics.example.com;
frame-src https://video.example.com

Notice what is missing: the inline cart script. We did not add 'unsafe-inline' to make it pass. MDN warns that 'unsafe-inline' defeats much of the purpose of having a CSP. The better fix is to move that script into a separate .js file on your own site, which 'self' already allows. If a theme or plugin you cannot change adds it, ask its developer.

But what about nonces? A nonce is a random token on each allowed inline script. MDN says it must differ for every HTTP response and must not be predictable, so a fixed line in .htaccess cannot do it. Nonces need support from your application code.

When the policy is ready, replace the line from Step 2 with this one. It must stay on one line inside .htaccess.

<IfModule mod_headers.c>
  Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://analytics.example.com; style-src 'self' https://fonts.example.com; font-src 'self' https://fonts.example.com; img-src 'self' https://cdn.example.com; connect-src 'self' https://analytics.example.com; frame-src https://video.example.com"
</IfModule>

Save, reload your pages, and check DevTools again. The list of violations should be much shorter. Repeat Steps 3 and 4 until only the things you want blocked are left.

Step 5 (optional): Send reports to your own endpoint

DevTools only shows what happens on your screen. To see what real visitors hit, the browser must send reports to a URL you control. Use an HTTPS URL on your own server that accepts POST requests and saves them, or ask your developer for one. No endpoint yet? Skip this step.

There are two reporting directives. report-uri is the older one and is deprecated. report-to is its replacement: it names an endpoint, and a Reporting-Endpoints header maps that name to a URL. At the time of writing (16 October 2026), MDN says report-to lacks full cross-browser support and recommends declaring both. MDN also says report-to reports arrive as application/reports+json and report-uri reports as application/csp-report, so your endpoint should accept both.

This Apache block adds the older report-uri directive, which needs no extra header. The newer report-to directive also needs a Reporting-Endpoints header. The Apache documentation I checked does not show how to put literal double quotes inside a Header value, so this guide gives no Apache line for it. Ask your host or developer to send the plain HTTP header below exactly as shown, and then add ; report-to csp-endpoint to the end of your policy.

<IfModule mod_headers.c>
  Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://analytics.example.com; style-src 'self' https://fonts.example.com; font-src 'self' https://fonts.example.com; img-src 'self' https://cdn.example.com; connect-src 'self' https://analytics.example.com; frame-src https://video.example.com; report-uri https://example.com/csp-reports"
</IfModule>

This is the header MDN shows for naming an endpoint, written as a plain HTTP line and not as Apache syntax.

Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"

Why this matters: MDN says endpoints on plain http:// are ignored, so always use https:// here.

Step 6: Read the reports

A report is a small JSON object. This made-up example from Sara's site follows the shape of the example in MDN's CSP guide, shortened.

{
  "type": "csp-violation",
  "url": "https://example.com/cart/",
  "body": {
    "blockedURL": "inline",
    "disposition": "reporting",
    "documentURL": "https://example.com/cart/",
    "effectiveDirective": "script-src-elem",
    "originalPolicy": "default-src 'self'; script-src 'self' https://analytics.example.com; report-to csp-endpoint",
    "sourceFile": "https://example.com/cart/",
    "lineNumber": 121,
    "sample": "document.querySelector('.cart')"
  }
}

The fields that matter most, using MDN's definitions:

  • blockedURL: the URL or type of the resource that broke the policy. In MDN's example report, inline is shown for an inline script.
  • effectiveDirective: the directive that caught it. This tells you which line of your policy to change.
  • documentURL: the page where it happened.
  • disposition: enforce for violations of the real Content-Security-Policy header, or reporting for the report-only header. During your test, you should see reporting.
  • sourceFile and lineNumber: for script problems, the file and line to look at.
  • sample: the first characters of an inline script or style, which helps you find it in your theme. External files do not get a sample.

Quick tip: sort your reports by blockedURL and count them. A source that appears on every page is probably something you need. A strange domain that appears only a few times deserves a closer look before you allow it.

How to check it worked

Run this command from your computer. It requests only the headers of your homepage.

curl -sI https://example.com/ | grep -i -e "content-security-policy" -e "reporting-endpoints"

After Step 2, you should see exactly this line. After Step 4, the same name followed by your full policy.

content-security-policy-report-only: default-src 'self'

The name may print in lowercase. That is fine: RFC 9110 says HTTP field names are case-insensitive.

Also check these:

  • The first line of the full curl -sI https://example.com/ output still shows a 200 status (or the same redirect as your "before" picture). If you see 500, roll back.
  • If your host or developer added the Reporting-Endpoints header from Step 5, the reporting-endpoints line shows csp-endpoint="https://example.com/csp-reports" with the inner quotes intact.
  • A page that does not exist, such as https://example.com/csp-test-404, also shows the header. That proves always is working on error pages.
  • Your pages look and behave exactly as before in the browser, because nothing is enforced yet.

Move from report-only to enforce

Give report-only mode a few days to a couple of weeks of normal traffic, so rare pages and different browsers get a chance to report. When the reports only show things you want blocked, switch on enforcement. This block replaces the report-only block with the enforced header, keeping reporting on.

<IfModule mod_headers.c>
  Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://analytics.example.com; style-src 'self' https://fonts.example.com; font-src 'self' https://fonts.example.com; img-src 'self' https://cdn.example.com; connect-src 'self' https://analytics.example.com; frame-src https://video.example.com; report-uri https://example.com/csp-reports"
</IfModule>

Want to test a stricter version later? Add a Content-Security-Policy-Report-Only line with the new policy next to it. Both are honored: one blocks, the other only reports.

No endpoint from Step 5? Remove the report-uri part at the end of the policy, and the report-to part if you added it.

Right after you switch, click through your key pages with DevTools open. Anything blocked now is really blocked, so fix it fast or roll back.

If you already set headers such as HSTS, keep them next to this block. Our guide to HSTS and security headers in .htaccess covers that setup.

Roll back in one step

If anything goes wrong, put the backup back. This command restores the copy from Step 1.

cd ~/public_html && cp -p .htaccess.bak-csp .htaccess

In the file manager, you can do the same by deleting the CSP block you added, or by renaming .htaccess.bak-csp back to .htaccess.

Then confirm the site is back. This command shows only the status line.

curl -sI https://example.com/ | head -n 1

You should see the same status as your "before" picture, usually HTTP/2 200 or HTTP/1.1 200 OK.

Troubleshooting

Problem: "500 Internal Server Error" right after saving

Symptom: every page shows a 500 error. Likely cause: a typo in .htaccess, such as a missing closing quote, a policy split over two lines, or curly "smart" quotes pasted from a word processor. It can also mean your host does not allow Header in .htaccess (no AllowOverride FileInfo). Fix: restore the backup first. Then paste the block again from a plain text editor and keep the policy on one line. If it still fails, ask your host whether mod_headers is loaded and FileInfo overrides are allowed for your account.

Problem: curl shows no CSP header at all

Symptom: the grep command prints nothing. Likely causes: mod_headers is not loaded, so the IfModule block is skipped silently. You edited an .htaccess in the wrong folder. Or a cache or proxy in front of your site is serving old headers. Fix: confirm the folder, test the uncached 404 URL above, purge any CDN cache, and ask your host whether mod_headers is enabled.

Problem: the header appears twice with different policies

Symptom: curl shows two content-security-policy-report-only lines. Likely cause: a plugin, your application, or another .htaccess rule also sends the header. Apache's docs say set replaces any previous header with this name, but a header sent by another source can still show up as a second line. Fix: find the other CSP source and keep only one.

Problem: fonts, analytics or videos break after you enforce

Symptom: text falls back to a plain font, the analytics dashboard drops to zero, or the embedded video shows a blank box. Likely cause: a source you did not see in report-only mode, often on a page you did not test, or a service that loads from more than one domain. Fix: open that page with DevTools, read the blocked URL and directive, and add the source to the matching directive (font-src, script-src plus connect-src, or frame-src). If it is urgent, roll back to report-only by changing the header name back, then fix calmly.

Problem: reports never arrive at your endpoint

Symptom: DevTools shows violations but your endpoint logs stay empty. Likely causes: the endpoint URL is http:// (ignored by the browser), the Reporting-Endpoints value lost its inner quotes (if it was added), or your endpoint rejects the application/reports+json or application/csp-report content types. Fix: check the exact reporting-endpoints line in curl, and test your endpoint with a POST of those types.

When to ask for help

Ask a developer if many inline scripts cannot be moved, or if you need nonces or a report collector. This guide uses Apache syntax only. I did not verify LiteSpeed or Nginx for it, so on those servers check their official documentation or ask your host first.

The one message worth forwarding

Copy this and send it to your developer or host:

"Hi, I want to add a Content Security Policy to example.com safely. Plan: send Content-Security-Policy-Report-Only from .htaccess first, collect violations for one to two weeks, then switch to Content-Security-Policy and keep a report-only copy for testing changes. Can you confirm mod_headers is enabled and AllowOverride FileInfo is allowed for my site, tell me if anything else already sends CSP headers, and help me move our inline scripts into separate files instead of using 'unsafe-inline'? I have a backup of .htaccess and a rollback plan. Thanks."

The takeaway: watch first, enforce later, and always keep a copy of .htaccess you can put back in ten seconds.

Reader questions

Can a report-only CSP break my website?

The policy itself cannot, because report-only mode blocks nothing. A typo in .htaccess can still cause a 500 error, which is why you back up the file first.

Can I set a report-only CSP with an HTML meta tag?

No. MDN states that Content-Security-Policy-Report-Only is not supported inside a meta element, so it must be sent as a real HTTP header.

Should I use report-to or report-uri?

Use both for now. report-uri is deprecated in favour of report-to, but at the time of writing MDN says report-to does not yet have full cross-browser support.

What happens if both CSP headers are sent at once?

The browser honors both. The Content-Security-Policy header is enforced, and the report-only header only generates reports.

Is adding 'unsafe-inline' a good quick fix?

MDN advises against it because it defeats much of the purpose of a CSP. Moving inline scripts into separate files on your own domain is the safer fix.

Does a CSP replace input validation?

No. MDN says CSP is not a replacement for careful input handling. It is an extra layer of protection.

Sources & further reading

  1. MDN: Content-Security-Policy-Report-Only
  2. MDN: Content-Security-Policy header and directives
  3. MDN: Content Security Policy guide
  4. MDN: Reporting-Endpoints
  5. MDN: CSPViolationReport
  6. W3C: Content Security Policy Level 3
  7. Apache HTTP Server: mod_headers
  8. Apache HTTP Server: core features (IfModule)
  9. Chrome DevTools: Issues panel
  10. RFC 9110: HTTP Semantics

Originally published . About our editorial updates.

Your next project deserves a better foundation.

Explore hosting built for your next chapter.

Explore hosting