Download our Free HTML Cheat Sheet - Quick Reference for Beginners

Content Security Policy (CSP) Explained: A Practical Guide

Posted 23 September 2026

A Content Security Policy (CSP) is a browser instruction that tells the page exactly where it's allowed to load scripts, styles, images, fonts and other resources from. Anything not on that list gets blocked automatically, before it ever runs. It's one of the few defences that still works even after an attacker has already found a way to inject code into your page - the browser simply refuses to execute it.

Most sites that get hit with cross-site scripting (XSS) don't get hit because the attacker wrote clever code. They get hit because there was nothing stopping an injected <script> tag from running once it was on the page. CSP is that stop.

How CSP works: an allow list, not a blocklist

CSP doesn't try to detect malicious code and block it. It works the other way around - you tell the browser what's allowed, and everything else is refused by default. That's a more reliable model than trying to spot "bad" scripts, because you're not guessing what an attack might look like. You're just saying: scripts load from here, images load from there, and nothing else runs.

The policy is a set of rules, one per type of content. Each rule is called a directive, and each directive gets its own list of sources. A typical policy might allow scripts from your own domain and Google Analytics, but block scripts from everywhere else - including scripts an attacker manages to inject inline.

The directives you'll actually use

There are more CSP directives than most sites will ever need. These are the ones that cover almost every real-world policy:

  • default-src - the fallback for anything not covered by a more specific directive below. If you only set one thing, set this.
  • script-src - which JavaScript is allowed to run.
  • style-src - which CSS is allowed to load.
  • img-src - which images are allowed to load.
  • font-src - which fonts are allowed to load.
  • connect-src - where the page is allowed to send AJAX/fetch requests and open WebSocket connections.
  • frame-src - which pages you're allowed to embed in an <iframe> on your site.
  • form-action - where a <form> on your page is allowed to submit to.
  • object-src - controls <object>, <embed> and Flash/Java plugin content. Most sites should block this entirely with 'none'.

One directive that trips people up is frame-ancestors. It sounds like the opposite of frame-src, and it is - frame-src controls what you're allowed to embed, frame-ancestors controls who's allowed to embed you. It's the modern, more flexible replacement for the old X-Frame-Options header, and it's how you stop your site being loaded inside someone else's iframe for a clickjacking attack. It has to be set in the actual HTTP header - it's one of a handful of directives a <meta> tag can't carry, which is covered below.

What goes inside a directive

Each directive takes a list of sources. The ones you'll use most:

  • 'self' - this exact website. Not subdomains, not other ports, not other schemes (http vs https counts as different).
  • 'none' - block it completely. No sources allowed at all.
  • https://example.com or *.example.com - a specific domain, or all its subdomains.
  • data: - allows data URIs, most commonly needed on img-src for base64-embedded images.
  • 'unsafe-inline' - allows inline <script> tags, onclick attributes and inline <style>. This is the one to be careful with - it's also exactly what lets an injected script run, so using it on script-src removes most of the protection CSP was meant to give you.
  • 'unsafe-eval' - allows eval() and similar. Same problem as above, avoid it if you can.
  • 'nonce-xxxxx' or 'sha256-xxxxx' - lets you allow one specific inline script or style by its random one-time value or its hash, without opening the door to every inline script. This is the correct way to handle a page that genuinely needs an inline script, instead of reaching for 'unsafe-inline'.

Build your policy instead of writing it by hand

If you'd rather tick boxes for what your site actually loads and copy out the finished header, our Content Security Policy Generator covers all nine directives above (frame-ancestors excluded, since it isn't something you configure per resource type) and outputs the code for Apache, Nginx, IIS, PHP, a plain HTML tag, or Next.js, Nuxt and Remix config.

Sending the policy: header vs meta tag

CSP works either as an HTTP response header or a <meta> tag in your <head>. The header is the better option whenever you can set one - it applies before the page starts parsing, and it's the only way to use frame-ancestors, report-uri/report-to, or sandbox, none of which a meta tag supports.

Apache (.htaccess)

Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self'; img-src 'self' data:"

Nginx

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self'; img-src 'self' data:" always;

IIS (web.config)

<system.webServer>
  <httpProtocol>
    <customHeaders>
      <add name="Content-Security-Policy" value="default-src 'self'; script-src 'self' https://www.googletagmanager.com" />
    </customHeaders>
  </httpProtocol>
</system.webServer>

PHP

header("Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com");

Meta tag

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://www.googletagmanager.com">

If you're on shared hosting and can't touch server config or PHP, the meta tag is a reasonable fallback - just remember it can't carry frame-ancestors or a report-uri, so you lose clickjacking protection and violation reporting.

Testing without breaking your site

Turning on a strict CSP without testing it first is a good way to silently break your own site - a blocked script just fails quietly, it doesn't throw an error your visitors will see. Before you enforce anything, use Content-Security-Policy-Report-Only instead of Content-Security-Policy:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'

In this mode nothing is actually blocked - the browser just logs what it would have blocked to the console (and to a reporting endpoint, if you've set report-uri or report-to). Browse your site with the console open, fix whatever shows up as blocked, and only switch to the enforcing header once the console's clean.

What usually breaks when you turn this on

  • Inline scripts and styles. Any <script> without a src, any onclick/onload attribute, and any style="..." attribute gets blocked unless you've allowed 'unsafe-inline' (not recommended) or given it a nonce/hash.
  • Third-party embeds. Google Analytics, Google Fonts, YouTube embeds, Stripe/PayPal checkout widgets, chat widgets - each one needs its own domain added to the relevant directive, and it's easy to miss one until it visibly breaks.
  • Base64 images. If your CSS or HTML embeds images as data:image/png;base64,..., img-src needs data: added or they'll stop rendering.
  • WordPress and other CMS plugins. Plugins that inject inline scripts (a lot of them do) will break under a strict script-src. You'll either need a nonce-based approach or to allowlist 'unsafe-inline' for script-src specifically, which weakens the protection for that one directive.

Is it worth setting up?

The honest answer: it depends on how much inline scripting your site already relies on. If you're running a mostly static site or a modern framework build, CSP is a genuine, low-maintenance layer of defence against XSS - once it's tuned, it mostly just sits there working. If you're running an older CMS covered in plugins that inject inline code everywhere, getting a strict policy working can take real effort, and a loose policy (with 'unsafe-inline' allowed) gives you a lot less protection for the trouble.

It's also worth being clear about what it doesn't do. CSP doesn't stop SQL injection, doesn't stop someone finding a vulnerable plugin and uploading a backdoor directly, and doesn't replace keeping software patched. It specifically closes off what an attacker can do if they manage to get a script onto your page - which is still one of the most common outcomes of a successful attack, but it's one layer, not the whole defence.

Frequently asked questions

What does 'self' mean in a CSP?

It means this exact website - not subdomains, not a different port, and http vs https counts as a different origin too. A resource on cdn.yoursite.com is not covered by 'self' on yoursite.com; you'd need to add it as its own source.

Will CSP break my website?

It can, if you turn it on without testing first - mainly by blocking inline scripts/styles and third-party embeds you forgot to allow. Use Content-Security-Policy-Report-Only while you're setting it up, fix what shows up in the console, then switch to the enforcing header.

What's the difference between frame-src and frame-ancestors?

frame-src controls what you're allowed to embed in an iframe on your own site. frame-ancestors controls who's allowed to embed your site in their iframe - it's what actually protects you from clickjacking, and it can only be set in the HTTP header, not a meta tag.

Should I use 'unsafe-inline'?

Only if you have no other option. It allows any inline script or style to run, which is close to what an XSS attack needs to work in the first place. If you genuinely need one specific inline script, a nonce or hash source lets you allow just that one without opening the directive to everything.

Can I add a CSP with just a meta tag?

Yes, and it's a reasonable option if you can't set custom headers. Just know it can't carry frame-ancestors, report-uri/report-to, or sandbox - so you lose clickjacking protection and violation reporting compared to the header version.

Rate this Article

 

Discussion

0 Comments

Be the first to start the discussion!