Content Security Policy (CSP) is an HTTP header that tells the browser where it's allowed to load scripts, styles, images, and other resources from. It's an effective defense against XSS attacks and data injection.
Basic CSP directives
| Directive | Controls |
|---|---|
| default-src | Default source for all resources |
| script-src | JavaScript |
| style-src | CSS |
| img-src | Images |
| connect-src | AJAX, WebSocket, Fetch |
| font-src | Fonts |
| frame-src | iframes |
| report-uri | URL for violation reports |
CSP policy examples
Strict policy (for simple sites)
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'
For WordPress with Google Fonts and GTM
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https://www.google-analytics.com;
Configuring via .htaccess
<IfModule mod_headers.c>
Header always set Content-Security-Policy " default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self';"
</IfModule>
Report-Only mode for testing
# Doesn't block, only sends reports
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report.php
Content-Security-Policy-Report-Only — collect violations for a week, then switch to a strict CSP without breaking the site.'unsafe-inline' and 'unsafe-eval' weaken protection. Use a nonce or hash for inline scripts in production.CSP violation messages explained
The browser always prints in the console which directive fired and what got blocked — there is no need to guess.
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.— an inline script in the HTML was blocked; the fix is to move the code into a separate file or sign the block with a nonce or hash.Refused to load the stylesheet 'https://fonts.googleapis.com/...' because it violates the following Content Security Policy directive: "style-src 'self'".— the font domain was never added tostyle-src, so styles from the external CDN will not load.Refused to connect to 'https://api.example.com/...' because it violates the following Content Security Policy directive: "connect-src 'self'".— an AJAX or fetch request to a third-party API was blocked; the domain has to be listed explicitly inconnect-src.Refused to frame 'https://widget.example.com/' because it violates the following Content Security Policy directive: "frame-src 'self'".— an embedded third-party iframe will not open until its domain appears inframe-src.- A JSON report sent to
report-urilike{"csp-report": {"document-uri": "...", "violated-directive": "script-src-elem", "blocked-uri": "...", "line-number": 42}}— this is how the browser reports a violation to the server: it shows which line and which resource triggered the block, even if the user never opened the console.
Ways to set CSP: where and when
| Method | Where it is set | frame-ancestors support | When to use it |
|---|---|---|---|
| Content-Security-Policy HTTP header | Server response (Nginx, Apache, PHP) | Yes, fully | The main method for production |
| <meta http-equiv> tag in HTML | Inside the page's <head> | No, the directive is ignored in a meta tag | When there is no access to server headers, for example on static hosting with no configuration |
| Content-Security-Policy-Report-Only | A separate server response header | Yes | Test mode: collects violations but blocks nothing |
| Enforce and report-only together | Two headers at once | Yes | Gradually tightening the active policy while collecting data on new violations |
When it is too early for a strict CSP
- The code is full of inline handlers like
onclick="..."and scattered<script>tags with no attributes — a strictscript-srcwithout'unsafe-inline'breaks them all at once; the fix is a gradual move to nonces or hashes, not flipping the switch overnight. - The site runs third-party widgets — a support chat, a payment form, an ad network — that inject their own inline code and pull resources from unpredictable domains; their sources have to be added to the policy by hand and kept up to date.
- The site is served through several CDNs with subdomains that change often — a fixed list of domains in the CSP goes stale quickly and needs constant manual syncing.
- There is no way to collect violation reports — turning on a strict policy without a
Report-Onlyperiod almost guarantees breaking some functionality quietly, until users start complaining.
Verifying that the policy is actually applied
# Confirm the header is really coming from the server
curl -I https://example.com | grep -i content-security-policy
# Open DevTools → Console on the live page:
# there should be no "Refused to" lines if the policy is in enforce mode
To check the policy itself for logical mistakes (overly broad sources, a missing object-src), it helps to paste the header value into an independent online CSP analyzer, such as Google's CSP Evaluator. If report-uri is configured alongside it, reports of violation attempts keep arriving even after the policy is in enforce mode, not just during the Report-Only stage.