Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of scripts, styles, images and other resources your page may load. If an attacker manages to inject a <script> into your page (cross-site scripting, XSS), a good CSP stops the browser from running it. It’s one of the most effective browser-side defences, and also one of the easiest to get wrong, because a strict policy can break your own site. The key is a gradual rollout.

A first look

Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

This allows scripts only from your own domain and one CDN, images from anywhere over HTTPS, no plugins, and no framing by other sites.

The main directives

Directive Controls
default-src Fallback for every fetch type not listed separately
script-src JavaScript
style-src CSS
img-src Images
font-src Web fonts
connect-src fetch/XHR/WebSocket destinations (APIs, analytics)
frame-src What your page may embed in iframes (YouTube, maps)
frame-ancestors Who may embed your page (clickjacking protection, replaces X-Frame-Options)
object-src Plugins: set it to 'none'
base-uri Restricts <base>: set it to 'self'
form-action Where forms may submit
upgrade-insecure-requests Rewrite http:// resource URLs to https://

Source values: 'self', specific origins (https://cdn.example.com), schemes (https:, data:), 'none', and the special keywords below.

Inline scripts: the hard part

Many sites use inline <script> blocks and onclick= attributes. CSP blocks them unless you allow them:

  • 'unsafe-inline' allows all inline scripts. That defeats most of CSP’s XSS protection for scripts, so avoid it in script-src.
  • Nonces: the server generates a random value per response, adds it to the header ('nonce-r4nd0m') and to each legitimate <script nonce="r4nd0m">. Injected scripts don’t know the nonce. This needs support in your app or templating.
  • Hashes: allow a specific inline script by its SHA-256 hash ('sha256-…'). Good for a few static snippets.
  • 'strict-dynamic': scripts trusted via a nonce or hash may load further scripts. It simplifies policies for modern apps.

'unsafe-inline' in style-src is a smaller risk, and is often accepted as a compromise.

Step 1: start in report-only mode

Content-Security-Policy-Report-Only doesn’t block anything. It only reports violations, in the browser console and, optionally, to an endpoint:

add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;

Browse your whole site: the homepage, logged-in areas, checkout and admin. Open the developer console to see what would be blocked. Collect reports from real users with report-uri or report-to, either with a reporting service or a simple endpoint that logs JSON.

Step 2: build the allow-list

For each legitimate violation, add the source, as narrowly as possible:

  • analytics: script-src https://www.googletagmanager.com; connect-src https://*.google-analytics.com;
  • embedded videos: frame-src https://www.youtube-nocookie.com;
  • fonts: font-src 'self' https://fonts.gstatic.com; style-src … https://fonts.googleapis.com.

Third-party tags (tag managers, chat widgets, ad networks) often load many more domains. Each one you allow widens the policy. That’s a good moment to question whether you need them.

Step 3: enforce

When the reports only show noise (browser extensions, injected ISP scripts), switch to the enforcing header:

add_header Content-Security-Policy "…same policy…" always;

Keep reporting enabled to catch regressions after site changes.

Adding the header

Nginx, in the server block, or in an include used by every location with its own add_header. See Nginx security headers:

add_header Content-Security-Policy "default-src 'self'; …" always;

Apache:

Header always set Content-Security-Policy "default-src 'self'; …"

From the application, if the app generates nonces (most frameworks have CSP middleware: Helmet for Express, django-csp, Laravel packages). Set the header in one place only, either server or app, to avoid two conflicting policies.

WordPress and CMS sites

CMS admin areas use lots of inline scripts and styles, so a strict CSP usually breaks them. Practical approaches:

  • apply a strict policy to the public site, and a lenient one (or none) to /wp-admin/;
  • use a security plugin that manages CSP with nonces;
  • at minimum, set frame-ancestors 'self', object-src 'none' and base-uri 'self', plus upgrade-insecure-requests. Those are easy wins that rarely break anything.

Test

curl -sI https://example.com | grep -i content-security-policy

Browser developer tools show violations in the console. Online CSP evaluators flag weak policies, for example 'unsafe-inline' scripts or wildcard sources.

Quick reference

Goal Directive
Block plugins object-src 'none'
Stop clickjacking frame-ancestors 'self'
Fix mixed content upgrade-insecure-requests
Safe inline scripts nonces or hashes
Test without breaking Content-Security-Policy-Report-Only

Related: HSTS and CORS explained.

  • Security
  • Web
  • Nginx