Jitsumi

CSP Report-Only: Test a Security Policy Before You Enforce It

Learn to audit Content Security Policy without breaking your site, interpret browser violation reports and move to enforcement with a practical test and rollback plan.

Illustration of a browser page being monitored before encountering a security enforcement gate.

A Content Security Policy (CSP) can stop unwanted scripts from executing in a browser. It can also break a working site if you enforce a policy that excludes a legitimate script, stylesheet or network connection. Start with the Content-Security-Policy-Report-Only response header, inspect the violations against real user journeys, and enforce a reviewed policy only after the expected features continue to work.

Report-only mode is a rehearsal, not a security shield. It reports actions that the proposed policy would restrict, while allowing those actions to proceed. The MDN guide to testing CSP explains that an enforced policy and a report-only policy can coexist; only the former blocks. The goal is to turn uncertain assumptions about your site's dependencies into a deliberate, tested rule set.

This guide is for developers responsible for a web application's HTTP responses in modern browsers, as of November 2026. The examples are illustrative and use reserved example domains; they are not measurements of Jitsumi's own infrastructure.

Know which policy the browser is following

A CSP is a set of browser-side restrictions delivered chiefly through an HTTP response header. Directives such as script-src specify where scripts may come from, while connect-src limits some network requests initiated by a document. CSP helps mitigate certain cross-site scripting (XSS) attacks, but it does not repair unsafe application code, remove the need for output encoding or replace authentication and authorisation. MDN's CSP overview describes these boundaries.

The two response headers have distinct jobs:

Header

Browser behaviour

When to use it

Content-Security-Policy-Report-Only

Monitors would-be violations without blocking the resources

Auditing a proposed policy or testing a change

Content-Security-Policy

Enforces the specified restrictions and can also report violations

Protecting users with a policy you have validated

A reporting header does not disable a separate enforced CSP already on the response. If the site has an existing enforced policy, that policy continues to block as normal, even while a second policy runs in report-only mode. Multiple enforced policies are cumulative rather than a way to loosen a restriction. This behaviour is specified in the W3C Content Security Policy Level 3 Working Draft, published on 27 October 2026.

Do not put Content-Security-Policy-Report-Only in an HTML <meta> element. Unlike an enforced CSP, the report-only policy must be sent as an HTTP response header, as the MDN reference states.

Start with one page and an inventory of what it loads

Before drafting a policy, open a representative page in a normal browser, including its authenticated state if relevant. Use Developer Tools → Network to identify external scripts, fonts, stylesheets, images, frames, workers and API connections. Include login, checkout, uploads, embedded video and consent banners where your application uses them.

A practical inventory records the resource, why the page needs it, whether you control it and how you will test it. A single successful home-page load is not evidence that your account recovery form or embedded payment step will survive enforcement.

For the first experiment, consider this proposed script policy:

Content-Security-Policy-Report-Only: script-src 'self'; object-src 'none'; base-uri 'self'

Here, script-src 'self' would permit scripts from the same origin but not a third-party script or an unapproved inline script. object-src 'none' would prohibit object embeds, and base-uri 'self' would restrict the document's base URL. This is an audit example, not a universally suitable production policy and not a claim that a same-origin script allowlist is a strong XSS defence. The web.dev strict CSP guide explains why nonce- or hash-based policies are generally preferable to broad host allowlists for XSS mitigation.

You will see violations in browser developer tools even before a reporting service is in place. To collect reports across sessions and browsers, configure a genuine endpoint instead of relying on a developer keeping the Console open.

Connect reporting to a real receiver

For a deployed site, the response can declare a named reporting endpoint and reference it from the proposed policy. This is the shape described by MDN's report-to documentation and its Reporting-Endpoints reference:

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

These are illustrative headers, not a drop-in production configuration. Replace the reserved hostname with a real HTTPS endpoint you control or a trusted reporting service. Make sure that receiver accepts and safely processes the report format used by each route before deploying the headers. The report-to mechanism sends Reporting API payloads, while the older report-uri mechanism sends a different JSON shape. They are not interchangeable parsers.

Why include both reporting directives? report-to is the newer mechanism, but compatibility with older browsers still matters. MDN recommends including report-uri alongside it during the transition; a browser supporting report-to ignores report-uri in the same policy. Check the current report-uri reference for its deprecated status and payload structure. Newer support for report-to should not be interpreted as universal support on every device.

Treat reports as untrusted input. Report bodies can contain document URLs, referrer information and, under certain policies, snippets of code. Apply request-size and rate limits, authentication or anti-abuse controls appropriate to your receiver, redaction and finite retention. Never render an unescaped report value inside an admin dashboard. MDN specifically warns that report-uri reports, including script samples, are attacker-controlled data.

If you cannot maintain a reporting endpoint, you can still run a useful local/staging audit with Developer Tools. Record the narrower evidence honestly: console checks on selected browsers are not production-wide observability.

A local experiment: see a violation without breaking the page

You can reproduce the difference between monitoring and enforcement using Python 3's standard HTTP server classes. This little server deliberately serves an inline script that is not permitted by its proposed script-src 'self' policy. It binds only to your computer's loopback address.

Save the following as csp_demo.py:

from http.server import BaseHTTPRequestHandler, HTTPServer

class Demo(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header(
            "Content-Security-Policy-Report-Only",
            "script-src 'self'; object-src 'none'; report-uri /csp-report",
        )
        self.end_headers()
        self.wfile.write(b"""<!doctype html>
<html><body>
<p>Check the browser console and document.body.dataset.probe.</p>
<script>document.body.dataset.probe = 'executed';</script>
</body></html>""")

    def do_POST(self):
        # Demo only: acknowledge the report without storing it.
        self.send_response(204)
        self.end_headers()

HTTPServer(("127.0.0.1", 8765), Demo).serve_forever()

Run python3 csp_demo.py and open http://127.0.0.1:8765/ in a browser. In its Console, evaluate:

document.body.dataset.probe

Expected in report-only mode: the value is "executed". The inline code still ran, and the browser should show a CSP report-only warning. The local /csp-report handler intentionally discards the incoming report; this example is about the browser's behaviour, not production report collection.

Next, stop the server and change only the header name to Content-Security-Policy. Restart, reload the page, and evaluate the same expression. Expected with enforcement: the value is undefined because the unapproved inline script is blocked. Review the Console and Network panels to see the reason. When finished, stop the server with Ctrl+C.

This is a small demonstration based on the documented semantics, not a substitute for testing your application's actual policies and flows. It intentionally uses an inline script so you have a predictable would-be violation; do not copy that pattern into an application you are trying to harden.

Interpret the reports rather than clearing them blindly

A violation report tells you what a browser detected against a policy. It does not automatically tell you whether the attempted action was malicious. A browser extension, a legitimate third-party script, a conditional feature or an injected payload can all generate reports; investigate before creating new allowances.

What you observe

What it suggests

Next useful check

A payment or sign-in flow triggers a script-src warning

The proposed policy may exclude a required script

Reproduce the exact feature with the vendor's integration guidance

An inline script warning appears on a known page

Current inline code has no matching nonce/hash permission

Inventory the script and plan a nonce- or hash-based migration

connect-src is reported during a real API action

The page is contacting an origin outside the proposed policy

Verify the intended API host, redirects and WebSocket use

Reports arrive only from some browsers

Reporting transport and client support may differ

Compare the client, the endpoint and each payload format

No reports arrive at all

The page might be compliant—or reporting may not work

Inspect the response headers and make a controlled test violation

Group findings by page, directive, browser version and observed user action. Before editing an allowlist, confirm that the resource is genuinely necessary and owned by the claimed provider. Adding * or 'unsafe-inline' just to silence a warning can undermine the protection you set out to deploy.

If a Console entry instead mentions CORS, that is a different permission system. Jitsumi's guide to diagnosing CORS preflights and credential failures explains how to inspect the actual request and response. A CSP connect-src restriction cannot be repaired by adding Access-Control-Allow-Origin to the API, and an API's CORS policy is not a replacement for CSP.

Strengthen the policy before switching enforcement on

A monitoring policy copied verbatim into enforcement can be too permissive for security or too restrictive for functionality. Use the reports to learn what the app loads, then design the policy that should actually protect it.

For many server-rendered applications, a strong target is a nonce-based script policy: generate a fresh unpredictable nonce per HTML response, put it on trusted script elements, and match it in the response header. Static sites may use carefully maintained script hashes. The strict CSP implementation guidance describes both approaches, including 'strict-dynamic' and fallback considerations. Do not use the same fixed nonce across users or reuse it across responses. Do not assume the sample 'self' allowlist is an equivalent replacement.

Keep other directives deliberate. For example, object-src 'none' and base-uri 'none' are frequently useful in a strict policy, but changing base-uri can affect an application that legitimately uses a <base> element. Avoid pasting an exhaustive default-src 'none' policy into production without checking every required resource category.

Check each change on staging first, including authenticated journeys, error pages, mobile layouts and third-party widgets. Then, where your deployment process supports it, release enforcement gradually with rollback ready. Continue monitoring after enforcement to catch browser differences, new dependencies and rarely used routes.

For a simple rollback, preserve the previously working response-header configuration in version control. If enforcement unexpectedly blocks a vital feature, restore that reviewed header configuration and redeploy through your normal process; do not globally disable browser protections or set unsafe-inline as an emergency shortcut without understanding the risk.

A practical readiness check

Before moving a candidate policy from report-only to enforcement, verify the following:

  1. The browser actually receives the intended response headers from the deployed origin after any CDN or reverse proxy; do not assume the application server's configuration reached visitors.
  2. A controlled test violation appears in a browser, and the reporting endpoint processes the intended format without leaking or endlessly storing sensitive URLs.
  3. You have reproduced important user journeys, including login, forms, integrations and paths that load different scripts.
  4. You have classified meaningful violations, fixed necessary code or integrations, and resisted broad allowances introduced only to suppress noise.
  5. The enforced version has been tested in staging, a rollback procedure exists, and monitoring will remain available afterwards.

To check the headers without changing site state, you can start with this command and inspect the final HTTP response:

curl -sS -D - -o /dev/null https://www.example.com/

Substitute your own public URL. curl displays response headers but does not enforce CSP; a normal browser test is still required. Repeat the browser check after deployment rather than assuming a configuration-file change took effect everywhere.

CSP is most useful when its restrictions reflect the application you actually operate. Report-only mode buys you the evidence and time to make that decision. The moment of success is not when every warning disappears: it is when legitimate features keep working and the browser enforces the restrictions you meant to ship.

Sources and further reading

Share this post