A CSP in report-only mode protects nothing
Report-only is a measuring tool, not a control. If nobody reads the reports it is a header that does nothing, and switching it on later is where the real work starts.
Content-Security-Policy has a report-only mode. You ship the policy, the browser tells you what it would have blocked, and nothing actually breaks. It is the correct way to start.
It is not a security control. It is a measurement, and a measurement nobody reads is just a header.
The state most shops are in
Report-only was switched on during a hardening pass, a reporting endpoint was configured or perhaps was not, and then attention moved elsewhere. Two years later the header is still there, still report-only, and the answer to "do we have a CSP" is technically yes.
An attacker injecting a script is not slowed down by a policy running in report-only. The browser executes the script and files a report. If nothing is reading the reports, the only outcome is that the attack was documented and permitted.
Enforcing it is where the work is
The reason it stays in report-only is that turning it on breaks things, and I would rather be honest about that than pretend it is a config flag.
From switching a policy from permissive to enforcing on this site in the last few days, three things I would tell anyone starting:
Every directive is a separate decision, and missing one fails silently. A blocked script does not raise an error the user or the log will show you. It simply does not run, and the feature it powered is quietly gone.
Third parties need naming individually, in more than one directive. An embedded tool typically needs its script origin, its connect origin, sometimes its frame origin and its image origin. Get four out of five right and it still does not work.
The legacy header interacts badly. X-Frame-Options has no allowlist form, so it cannot express "only this one origin may frame us". Where both headers are present, frame-ancestors is the one browsers obey, so leaving the old one set is at best noise and at worst a broken embed you spend an afternoon on.
Where the policy lives matters too
One that cost me a deploy: on some hosting the CSP is not just a runtime header, it is compiled into a static configuration file that the platform validates. A value that is fine in your application config can be rejected outright at deploy time, after a successful build, with an error pointing at a line number in a file you did not write.
Worth knowing where your headers actually come from before you start changing them.
The short version
Report-only is a stage, not a destination. Read the reports for a fortnight, fix what they show, then enforce. If you are not going to do the second half, the header is costing you a small amount of bandwidth and buying you nothing.
