The idea
By default, a web page will run any script it is handed. It does not care whether you put it there or whether someone else did.
That is the whole basis of web skimming. Attackers get one script onto a checkout page, and the browser runs it as happily as it runs yours. Card details go out the side door while the page looks completely normal.
CSP flips the default. Instead of "run anything", the page runs only what you have listed. Injected code has nowhere to execute from.
Be warned: this one really does break sites
Most security headers are cheap. You set them, nothing changes, you move on.
CSP is not like that. It is an allowlist for everything your page loads, and a real site loads more than you think: analytics, fonts, chat widgets, payment scripts, an embedded map, three things the marketing team added in 2023 that nobody remembers. Miss one and it silently stops working.
This is not a reason to skip CSP. It is a reason to introduce it in stages rather than pasting a strict policy off a blog and hoping.
The ladder
Climb it in this order. Do not jump.
- 1.Observe. Ship it as Content-Security-Policy-Report-Only. The browser enforces nothing and reports what would have been blocked. You get an honest inventory of what your site really loads, which is almost never the list you would have written from memory.
- 2.Ship something permissive but real. Move to the enforcing header with a policy that shuts real doors while breaking almost nothing. This is the step people skip, and it is the one with the best ratio of protection to pain.
- 3.Tighten. Work toward default-src 'self' with nonces on the scripts you actually need. This is the version that stops injected scripts properly, and getting there takes time.
The policy worth shipping first
This is the step two policy. Short, useful, and unlikely to break anything.
Content-Security-Policy: object-src 'none'; frame-ancestors 'self'; base-uri 'self'object-src none kills legacy plugin embeds. frame-ancestors self stops other sites framing yours, which is the modern replacement for X-Frame-Options. base-uri self stops an injected base tag quietly re-pointing every relative URL on the page. Note that if two policies are sent, browsers apply the intersection of both, so set CSP in one place only. Setting it in a plugin and the server config at the same time will produce a policy stricter than either.
Two honest caveats
unsafe-inline defeats much of the point. It permits inline scripts, which is exactly what injected code tends to be. But on a themed or plugin heavy site, inline scripts are everywhere and often outside your control, so removing them is a project rather than a setting. If you need it for now, keep it, and get the rest of the policy in place. A CSP with unsafe-inline still beats no CSP. Just do not tell yourself you are finished.
The meta tag version only works partially. You can deliver CSP with a meta http-equiv tag, which matters on platforms that will not let you set headers. But frame-ancestors, report-uri and sandbox are ignored in meta form. So the frame protection above only works as a real header. If meta is all you have, use it, and know what you are not getting.
Where to go next
CSP sits alongside a handful of other security headers that are far easier to switch on. If you want the frame protection specifically, read X-Frame-Options for why you still send both.
Our free check will show you which headers your site sends today, so you know where you are starting from.
We have step by step instructions for every major platform, including the ones that will not let you.
See how to add a CSP on your platform