Setting Up CSP Headers That Do Not Break Your SPA
Content-Security-Policy is one of the most effective security headers you can add to a web app, and it is also the one people give up on fastest, because turning it on wrong breaks the site immediately and loudly. Here is a path that does not do that.
What CSP actually does
CSP tells the browser which sources of content, scripts, styles, images, fonts, are allowed to load and run on your page. If an attacker manages to inject a <script> tag through an XSS bug, a strict CSP stops that script from executing even though the injection succeeded. It is a second line of defense, not a replacement for fixing the injection bug itself.
The policy is a header, Content-Security-Policy: <directives>, and the directives are a list of rules per resource type. script-src 'self' means only scripts from your own origin can run. default-src 'none' blocks everything not explicitly allowed elsewhere.
The three mistakes that break every SPA on the first try
unsafe-inline looks like the fix and is actually the problem. If your build has inline <script> tags or inline event handlers, the easy fix is adding unsafe-inline to script-src. Do not do this. It defeats most of what CSP protects against, because now any injected inline script runs too. The real fix is to move to external script files or nonce-based inline scripts, not to whitelist all inline scripts.
eval and its friends. eval(), new Function(), and some templating libraries use dynamic code execution under the hood. CSP blocks this by default unless you add unsafe-eval, which you also should not do if you can avoid it. Most modern frameworks do not need it in production builds; check whether a dev-mode-only dependency is the actual culprit before reaching for the unsafe flag.
Blob and data URLs. Web workers, PDF viewers, and some chart libraries load content from blob: or data: URLs. A default-src policy blocks these unless you explicitly allow them. This one is legitimate more often than the first two, but it still needs to be a deliberate worker-src blob: addition, not a blanket allowance.
Start in report-only mode, always
The header that changes behavior is Content-Security-Policy. The header that just tells you what would have broken is Content-Security-Policy-Report-Only. Use the second one first, always, no exceptions, even if you are confident about your policy.
Set up a report-uri or the newer report-to directive pointing at an endpoint that logs violations. Deploy the report-only header to production and let it run for at least a few days, ideally across a full weekly traffic cycle so you catch anything that only happens on, say, a batch job that runs Sunday night.
Read the reports. You will see things you did not expect, a third-party analytics script loading from a CDN you forgot about, an old inline onclick handler nobody noticed because it worked fine without CSP.
Building the actual policy
For most SPAs shipping external bundles with no inline scripts, a workable starting policy looks like this:
style-src 'unsafe-inline' is still relatively common because a lot of SPA frameworks inject inline styles for dynamic layout, and the risk profile of inline CSS is much lower than inline JS. If your framework supports nonces for styles too, use those instead and drop the unsafe flag.
object-src 'none' and frame-ancestors 'none' are close to free wins, almost nobody needs Flash-era plugin embeds anymore, and blocking your site from being framed by someone else kills a whole class of clickjacking attempts.
From report-only to enforced
Once the reports stop surfacing anything you did not expect, swap the header from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the report-only header running alongside it for a while with a slightly looser policy, so you get advance warning before you tighten further.
Watch your error monitoring for the 48 hours after the switch, not just the CSP reports. A blocked script sometimes fails silently in a way that only shows up as a broken feature, not a console error your reporting endpoint catches.
Where this connects to the rest of the stack
CSP is one line item on a longer checklist of security headers, and it is genuinely worth doing on its own, independent of anything else. Our scanner flags missing or unsafe CSP configuration as part of its broader IDE checks, but the work of actually building a correct policy for your app is exactly what this post walked through, and it does not need a tool to do it. It needs the report-only header, a few days of patience, and reading the reports before you flip the switch.