How to Roll Out CSP Without Breaking Your Site — 3 Stages From Report-Only to Enforcement
Content-Security-Policy (CSP) is a security header with a proven track record, but in practice the most common failure story is "the policy was misconfigured and it took the site down." Miss just one third-party widget or one inline style, and that whole resource gets blocked outright. That's why the standard approach with CSP isn't to flip on enforcement mode immediately — it's to go through an observation-only stage first and narrow the policy down gradually.
1. Why turning on enforcement mode first is risky
The Content-Security-Policy header immediately blocks any resource load that violates the policy. The problem is that, in a lot of real production sites, even the developers themselves don't fully know every script, stylesheet, image, and font their site loads and from where. Hidden dependencies lurk in places you wouldn't notice — an ad script dynamically pulling a sub-resource from yet another domain, or a CMS editor auto-inserting inline style attributes. If you enforce the policy from the start, every one of these hidden dependencies that gets caught can break the page or silently stop a feature from working — and the worst possible order of discovery is having users find and report it first.
2. Stage 1: observe only, with Report-Only
The standard way to avoid this problem is the Content-Security-Policy-Report-Only header. True to its name, it never blocks a resource for violating the policy — it only collects the fact that a violation happened, as a report. When the browser detects a violation, it sends a JSON violation report to the endpoint specified in the policy, either report-uri (the older mechanism) or report-to (the newer one).
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.example.com; report-uri /csp-reportIn this state, a script loaded from a domain not in the policy will still actually load and execute — but a violation report gets fired off to
/csp-report. The site keeps behaving exactly as usual while you get to observe, in advance, "what would have broken if this policy had been enforced."
At this stage, running your draft policy through the CSP Validator first lets you catch structural problems — a typo in a directive name, a missing default-src — before the reports even start piling up.
3. Stage 2: analyze the reports and refine the policy
After running Report-Only for a few days to a few weeks, violation reports pile up showing exactly which resources in real traffic conflict with the policy. As you review them, they split into two branches.
- A legitimate resource the policy was too narrow for: add that domain to the allowlist of the appropriate directive, such as
script-srcorstyle-src. - A resource that shouldn't have been there in the first place (a malicious injection, an abandoned legacy script, etc.): leave the policy as-is, and remove the resource load from the actual code instead.
If you skip this stage and jump straight to enforcement, you end up sorting these two branches after the site has actually broken, not before. That "advance sorting" is the entire reason the Report-Only stage exists.
4. Stage 3: switch to enforcement mode
Once the reports settle down and the policy looks like it matches real traffic patterns, switch the same policy string over by just changing the header name to Content-Security-Policy. From this point on, policy violations are actually blocked. It's a good idea to keep report-uri in place right after the switch too — violation reports keep coming in even under enforcement mode, and that channel stays useful as an early-warning system for catching a newly added feature that breaks the policy.
| Stage | Header | Behavior | Purpose |
|---|---|---|---|
| 1 | Content-Security-Policy-Report-Only | No blocking, reports only | Observe the blast radius |
| 2 | (same, iterated repeatedly) | No blocking | Refine the policy from the reports |
| 3 | Content-Security-Policy | Actually blocks on violation | Enforcement, with reports still collected |
5. data: URIs carry completely different risk levels depending on the directive
One judgment call you'll run into repeatedly while refining a policy is whether to allow a data: URI. Even though it's the same data: scheme, the risk swings widely depending on which directive you put it in.
- data: in img-src: used to inline base64-encoded images. It's not executable code, so it's relatively safe. Commonly used for inlining icon sprites or small thumbnails.
- data: in script-src / style-src: becomes a route for an attacker to run arbitrary code via something like
<script src="data:text/javascript,...">, bypassing your inline-script block (not allowingunsafe-inline). In other words, even if you carefully removedunsafe-inlineto block XSS, leaving data: open in script-src effectively reopens the same hole through different syntax.
Pasting a policy into the CSP Validator uniformly flags usage of unsafe-inline, unsafe-eval, and wildcards (*) regardless of which directive they're in — but it doesn't automatically make the directive-specific contextual judgment that "data: is safe in this directive but risky in that one." When drafting a policy, it's safest to use the tool's warning list as a first-pass filter and check which directive each data: URI sits in yourself, using the criteria above.
Frequently Asked Questions
Q. Is it fine to leave the Report-Only header on forever and never switch to enforcement mode?
No, that provides no security benefit. Report-Only is purely for observation and never actually blocks anything, so you have to eventually switch to Content-Security-Policy for CSP to serve any real defensive purpose.
Q. Can I send the Report-Only header and the enforcement header at the same time?
Yes, you can send both headers simultaneously. It's common to run them in parallel — enforcing an already-stabilized, strict policy via Content-Security-Policy, while separately observing an even stricter experimental policy you're working toward next via Report-Only.
Q. Should I use report-uri or report-to?
report-uri is older but has broader browser compatibility; report-to is based on the newer Reporting API and is more flexible, but requires a separate Report-To header configuration. If compatibility matters most, keep report-uri; on a modern stack, it's common to specify both to have a fallback.
Q. How do I validate a draft policy?
Pasting your header value into the CSP Validator parses it directive by directive and flags unsafe keywords, wildcards, and a missing default-src. You can use it to catch structural typos or omissions before deploying with Report-Only.