What a Content-Security-Policy actually does (and why 'medium risk' undersells a missing one)

Run almost any website security scanner and "No Content-Security-Policy" is one of the more common findings you'll see. It's easy to skim past — no red siren, no "critical" stamp on some scanners, just another line in a list of headers. That undersells what it actually does.
What the browser does without one
Without a Content-Security-Policy (CSP) header, a browser will run any JavaScript that ends up on the page — no matter where it came from. If your own code has an unfiltered input that lets an attacker inject a <script> tag (classic XSS), or if a third-party library you depend on gets compromised and starts injecting malicious code, the browser has no rule to check that request against. It just runs it, with full access to cookies, session tokens, and anything else on the page.
A CSP header is the browser-level rule that closes that gap: an explicit allowlist of exactly which sources of scripts, styles, images, and other resources this page is allowed to load from. Anything not on the list gets blocked, silently, before it executes.
Why it's easy to leave out
The honest reason most sites don't have one isn't that nobody thought of it — it's that getting it right takes real auditing. A CSP that's too strict breaks the page (a chat widget stops loading, an analytics script silently fails, a payment form breaks). A CSP that's too loose (script-src *) is barely better than having none at all. So teams either skip it entirely, or ship a broken one, disable it, and never come back to it.
How we actually built ours
The only reliable way to write a CSP that doesn't break your own site is to audit exactly what your pages actually load, then write the policy against that real list — not a generic template. Concretely: pull every <script src> and <link rel="stylesheet"> tag across your pages, note every third-party domain that shows up (a payments SDK, an analytics tag, a font provider, an error-tracking script), and build the allowlist from that exact set. Then deploy it and actually check the browser console for CSP violation warnings before calling it done — that's the step that catches whatever the static audit missed (a script injected dynamically at runtime, for instance).
One realistic compromise worth naming: many sites, ours included in places, still rely on inline <script>/<style> tags scattered through existing pages. A fully strict CSP would require rewriting every one of those to load from external files or use per-request nonces — a large refactor on its own. Allowing 'unsafe-inline' for script/style sources is a real, honest trade-off: it still blocks arbitrary third-party script injection (the more common real-world attack), just not injection into your own inline code specifically. Better than nothing, not the theoretical ideal — and worth revisiting once inline scripts are cleaned up.
The practical takeaway
If a scanner flags a missing CSP as "medium," don't read that as "low priority." Read it as: your site currently has no browser-level barrier against a category of attack (XSS) that's one of the most common ways real breaches start. It's not a quick toggle — it requires actually knowing what your pages load — but it's a few hours of real auditing, not a redesign, and it's one of the few security headers that changes what an attacker can actually do, not just what they can see.