Blog
SavedSearch
Visit Hostlelo

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

Stage report-only CSP on Apache with a working collector; test violations and critical flows before enforcement, with rollback ready.

The short answer

Stage a Content-Security-Policy-Report-Only header from .htaccess with a working HTTPS report collector. The policy itself blocks nothing; its reporting directive sends violations for review. Test representative flows and reporting before enforcing a refined policy, with a verified .htaccess rollback path.

A browser card and shield send policy alerts into report sheets that finish with a green check, illustrating CSP report-only testing.
Reading options

On this page

You can add a Content Security Policy to an Apache site without breaking it by sending it first as Content-Security-Policy-Report-Only. That policy itself blocks nothing, but it needs a reporting destination to give useful violation reports. It can tell 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, a desktop browser with developer tools, and an HTTPS CSP report collector you control that accepts POST requests. If you do not have a secure collector, ask your host or developer to provide one before following the reporting steps; a header alone does not store reports.
  • Backup: a copy of your current .htaccess (Step 1 below).
  • Time: about 15 minutes to add the header if a collector is ready, then a few days or longer of testing and reviewing reports. Setting up a secure collector is a separate task.
  • 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. A private backup enables rollback, but verify the response after restoration and clear any stale CDN copy if necessary.

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

Find .htaccess in the site's document root. Enable hidden-file display if necessary. Copy it over SFTP to private storage, or use a host-confirmed backup parent outside every document root, alias and symlinked public path. Do not put a .bak copy beside the live file.

This illustrative Bash example runs from the document root after you replace the private parent with a confirmed existing writable directory. It has not been executed on your server:

(
  set -eu
  umask 077
  private_parent='/absolute/non-public-backups'
  test -d "$private_parent" && test -w "$private_parent"
  backup_dir="$(mktemp -d "$private_parent/csp.XXXXXXXX")"
  cp -- .htaccess "$backup_dir/htaccess.backup"
  chmod 600 "$backup_dir/htaccess.backup"
  ls -ld "$backup_dir" "$backup_dir/htaccess.backup"
)

Record the absolute backup path and the live file's original ownership and permissions. Expect owner-only access to the backup directory and mode 600 on its file. Keep a protected off-server copy. Neither a home directory nor a dot-prefixed filename is automatically private.

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

Before editing .htaccess, replace https://example.com/csp-reports with your own working HTTPS collector. A report-only policy without report-uri or report-to may not produce useful violations; a quiet console is not proof that the policy is safe. Confirm the collector accepts and protects reports before sending real traffic to it.

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 and sends violations to the HTTPS collector. Replace the example collector address; do not publish it unchanged.

<IfModule mod_headers.c>
  Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri https://example.com/csp-reports"
</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 modifies the header in Apache's always table, including error responses. It replaces an earlier value only within that table; another table or upstream can still produce a duplicate. Inventory existing CSP sources and inspect the final headers instead of assuming set removes every duplicate.
  • The policy sits inside double quotes, and 'self' keeps its single quotes.

Save the file, reload a representative page and check both DevTools and the collector for an expected violation. If neither shows one, create a controlled test on a non-sensitive page or ask a developer to do so; silence alone does not prove the policy is working. The report-only policy should not block resources, but an existing enforced CSP may still do so.

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 browser violations and collector reports

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; report-uri https://example.com/csp-reports"
</IfModule>

Save, reload your pages, and check DevTools again. The list of violations should be much shorter. Check that reports still arrive at your collector. Repeat Steps 3 and 4, testing critical routes and browser variants, until the remaining reports are understood; lack of reports alone is not enough.

Step 5 (optional): Add report-to alongside report-uri

The policy above already uses report-uri to send reports to your collector. If you want the newer Reporting API format, a developer can add report-to with a matching Reporting-Endpoints header while retaining report-uri for browser fallback. This is optional; do not add report-to until the endpoint can accept and distinguish both report formats.

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.

The Step 4 Apache block already includes 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.

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": "report",
    "documentURL": "https://example.com/cart/",
    "effectiveDirective": "script-src-elem",
    "originalPolicy": "default-src 'self'; script-src 'self' https://analytics.example.com; report-uri https://example.com/csp-reports; report-to csp-endpoint",
    "sourceFile": "https://example.com/cart/",
    "lineNumber": 121
  }
}

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 an enforced policy, or report for a report-only policy. The CSP specification defines these exact values; reporting is not the disposition value.
  • sourceFile and lineNumber: for script problems, the file and line to look at.
  • sample: this field is usually absent. It is populated for some inline script or style violations only when the relevant script-src or style-src directive includes 'report-sample'. External files do not get a sample. Do not add 'report-sample' casually: snippets may contain sensitive data, and violation reports are attacker-controlled input that your collector must validate and render safely.

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, the header should contain both default-src 'self' and your own HTTPS report-uri; after Step 4 it should show the full policy with that same reporting destination. If the destination is missing, do not interpret a quiet report stream as success.

content-security-policy-report-only: default-src 'self'; report-uri https://example.com/csp-reports

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

Observe representative traffic for days to weeks, but do not treat a quiet collector as proof that every route, browser and user flow is covered. Before enforcing, verify the collector works, exercise checkout/login/forms/embeds, inspect any existing enforced policy, review the reports, arrange a maintenance window and retain the exact rollback path. Then switch the header name in the block below; continue reporting and test immediately.

<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.

If you do not have a functioning HTTPS collector, stop before enforcement and ask your host or developer to provide one. Removing both reporting directives would leave you without visibility into violations; it is not a substitute for testing.

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 the site fails after this change, restore .htaccess from the exact private backup path recorded in Step 1, using SFTP or your file manager. Restore only this file and preserve the live file's original owner and permissions. If you use SSH, replace both placeholders in this illustrative command with the verified paths:

cp -- '/absolute/non-public-backups/csp.XXXXXXXX/htaccess.backup' '/absolute/path/to/document-root/.htaccess'

Do not guess the generated directory name or expose the backup through a public download URL. Remove the specific temporary server backup after confirming the protected copy and your retention requirements.

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 the collector has no reports. Confirm the report-only header actually includes report-uri or report-to, that the HTTPS endpoint exists and accepts POST, and that a controlled violation reaches it. If report-to was added, verify the Reporting-Endpoints value kept its inner quotes and that the collector accepts application/reports+json as well as application/csp-report for report-uri fallback. Apply request-size limits, rate limiting, restricted access, retention and safe rendering; report payloads can be attacker-controlled and may include sensitive page URLs.

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 stage a Content Security Policy for example.com safely. Please inventory existing CSP headers first, preserve their protections, and configure a working HTTPS collector with access controls, request limits and retention. Start with Content-Security-Policy-Report-Only and verify a controlled report reaches the collector. Test representative login, checkout, forms and embeds, review violations, and agree a rollback plan before considering enforcement; elapsed time or quiet reports alone are not enough. Please confirm mod_headers and AllowOverride FileInfo and help replace unsafe inline code appropriately. I have a private .htaccess backup. Thanks."

The takeaway: verify the reporting path, test representative flows, and enforce only after reviewing the evidence. Keep a private backup and verify recovery rather than promising a fixed rollback time.

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. Content-Security-Policy is enforced; Content-Security-Policy-Report-Only does not block and generates reports when a working reporting destination is configured.

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

What changed

Added an original article cover and matched the visible cover, social image and article schema image.

Originally published . About our editorial updates.

Your next project deserves a better foundation.

Explore hosting built for your next chapter.

Explore hosting

Find your next answer.

Search published guides and articles.

Ctrl / ⌘ K to open · Esc to close