Content Security Policy: a bouncer for your web pages
The single most powerful protection against malicious scripts being injected into your site — and why most small business sites do not have one.
The short version
The problem it solves
The most common way websites are attacked is not by breaking into the server. It is by sneaking a small piece of malicious code onto a page so that it runs in your *visitors'* browsers. That is cross-site scripting (XSS). It happens through a vulnerable plugin, a comment form that does not clean its input, a compromised third-party widget, or an advert network that got infected.
Once the attacker's script is running in a visitor's browser, on your page, it can do anything your page can: read what they type (including card numbers in a checkout), steal their login cookie, quietly redirect them, or replace your payment form with a lookalike. The infamous Magecart attacks that skimmed card details from British Airways and Ticketmaster were exactly this.
Here is the thing: you cannot guarantee you will never have an XSS hole — plugins, themes and third-party scripts are all out of your direct control. A CSP accepts that and adds a second line of defence: even if malicious code gets onto the page, it does not get to run.
How it works, with an analogy
Think of a CSP as the guest list at the door of a private event. Your page says to the browser: "Scripts are allowed from my own domain and from Google Analytics. Images from my domain and my image CDN. Fonts from Google Fonts. Nothing else." The browser is the bouncer. When a script turns up from evil-attacker.net, it is not on the list, and it does not get in — no matter how it got onto the page.
A policy is just a header sent with each page. A reasonable starting point for a typical business site looks like:
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data: https:; frame-ancestors 'none'
Each part names a *type* of thing (scripts, styles, images, fonts) and the places it may come from. 'self' means your own domain.
The catch: unsafe-inline
Many sites, especially WordPress sites, have small bits of JavaScript written directly inside the page rather than in separate files — "inline" scripts. A strict CSP blocks those too, because an injected attacker script is *also* inline. The lazy way round it is to add 'unsafe-inline' to the script list, which allows all inline scripts — including the malicious ones. That is why we flag unsafe-inline in script-src as weakening the policy: it reopens the exact door the CSP is meant to close.
The proper fix is to move inline scripts into files, or to use nonces or hashes — a one-time code on each legitimate inline script that the policy whitelists. This is more work, and for a small site it is reasonable to start with a policy that allows inline *styles* (much lower risk) but keeps *scripts* strict.
How to add one without breaking your site
A CSP that is too strict will block things your site needs — a chat widget, an embedded map, your analytics — and parts of the page will stop working. So the safe way to roll one out is in stages:
- Start in report-only mode. Send the header as
Content-Security-Policy-Report-Onlyinstead. The browser logs what *would* have been blocked (in the developer console, and optionally to a reporting address) but blocks nothing. Your site keeps working exactly as before. - Browse your site for a week and collect what gets reported — every legitimate third-party script, image host and font you use.
- Add those sources to the policy, keep the rest locked down, and test the key pages (home, contact form, checkout).
- Switch the header to enforcing (
Content-Security-Policy) once the reports are quiet.
On Apache/LiteSpeed hosting you set the header in .htaccess with Header always set Content-Security-Policy "...". On WordPress, plugins such as "HTTP Headers" or a security plugin with a headers section can manage it. Cloudflare users can add it under Rules → Transform Rules → Modify Response Header.
Common questions
My site is simple and has no logins or payments. Do I need a CSP?
Will a CSP stop my Google Analytics / chat widget / embedded map?
Why does VigilScan flag unsafe-inline but not the rest of my policy?
Not sure if this affects your site?
Run a free check — it takes about 90 seconds and tells you exactly where you stand.