Blog

Web Security articles

Content Security Policy (CSP): A Practical Introduction

A practical guide to Content Security Policy: key directives, nonces, report-only mode and a step-by-step way to deploy CSP without breaking your site.

4 min read Web Security

Cross-site scripting (XSS) happens when an attacker manages to get their own JavaScript running on your web page, perhaps through a comment form, a vulnerable plugin or a compromised third-party script. Once it runs, it can steal session cookies, capture keystrokes or redirect visitors. A Content Security Policy (CSP) is a response header that tells the browser exactly which scripts and other resources your page is allowed to load and run. Anything not on the list is blocked, which turns many XSS bugs from a breach into a harmless console error.

What a Content Security Policy looks like

A policy is a list of directives, separated by semicolons. Each directive names a resource type and the sources allowed for it:

Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example-cdn.com; script-src 'self' https://www.googletagmanager.com

Read it as: by default, load everything only from my own origin; images may also come from one CDN; scripts may also come from Google Tag Manager. A script tag pointing anywhere else, or inline script code, will be refused.

The directives you will use most

DirectiveControls
default-srcFallback for any fetch directive you do not set explicitly
script-srcJavaScript files and inline scripts
style-srcStylesheets and inline styles
img-srcImages
connect-srcRequests made by scripts (fetch, XMLHttpRequest, WebSocket)
font-srcWeb fonts
frame-srcWhat your page may embed in iframes (videos, maps, payment forms)
frame-ancestorsWhich sites may embed your page (clickjacking protection)
object-srcPlugins via <object> and <embed>; usually set to 'none'
base-uriRestricts the <base> tag, which attackers can abuse to redirect relative URLs
form-actionWhere forms may submit data

Common source values include 'self' (your own origin), 'none' (nothing), a specific host such as https://cdn.example.com, and a scheme such as https: or data:. The quotes around keywords like 'self' are required; without them, the browser reads self as a hostname.

The inline script problem

By default, a CSP that sets script-src blocks all inline JavaScript: code inside <script> tags, onclick= attributes and javascript: links. That is the whole point, because injected XSS payloads are almost always inline. But many websites rely on legitimate inline code, especially analytics snippets and CMS themes.

You have three ways to handle this:

  1. Move inline code into external files. The cleanest option, though not always practical on an existing site. If you are planning a new web application, avoiding inline scripts from day one makes a strict policy far easier.
  2. Nonces. The server generates a fresh random value for every response, puts it in the header, and adds it to each legitimate script tag:
    Content-Security-Policy: script-src 'nonce-R4nd0mV4lu3' 'strict-dynamic'; object-src 'none'; base-uri 'none'
    
    <script nonce="R4nd0mV4lu3">...</script>
    An attacker cannot guess the nonce, so their injected script is blocked. The nonce must be unpredictable and different on every page load; a fixed nonce gives no protection. Adding 'strict-dynamic' lets scripts you trusted load further scripts, which makes nonce-based policies workable with tag managers and modern frameworks.
  3. Hashes. For static inline scripts that never change, you can allow a specific script by its SHA-256 hash, written as 'sha256-...'.

Avoid 'unsafe-inline' in script-src. It allows every inline script, including an attacker's, and removes most of the XSS protection. Similarly, 'unsafe-eval' allows eval() and should be avoided where possible.

Deploying CSP without breaking your site

The single most important tool is report-only mode. Using a different header name, the browser evaluates your policy and reports violations but blocks nothing:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

A practical rollout looks like this:

  1. Inventory what loads. Open your key pages with the browser's developer tools on the Network tab and list every third-party domain: analytics, fonts, chat widgets, payment providers, video embeds.
  2. Draft a policy that allows those sources and nothing else.
  3. Deploy it in report-only mode. Violations appear in the browser console and, if you set report-uri or the newer report-to, are posted as JSON to your endpoint.
  4. Refine for a week or two, covering checkout, login, admin and any page with embedded content.
  5. Switch to enforcement by renaming the header to Content-Security-Policy. Keep a report-only header running alongside if you want to test tighter changes later.

Our HTTP header checker lets you confirm the header is actually being sent on each page, which is worth checking because caching layers and CDNs sometimes strip or override headers.

A sensible starting policy

For a simple site that hosts its own assets, this baseline blocks a lot without much effort:

Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests

upgrade-insecure-requests tells the browser to fetch any http:// resources on the page over HTTPS instead, which also helps with mixed content. Add specific hosts as your report-only testing reveals them.

Common mistakes

  • Forgetting that default-src does not cover frame-ancestors, base-uri or form-action; set those explicitly.
  • Allowing broad sources like https: or a large CDN domain in script-src, which may host attacker-controlled files.
  • Setting CSP in an HTML <meta> tag and expecting frame-ancestors or reporting to work; those only work in the HTTP header.
  • Treating CSP as a replacement for fixing XSS. It is a second line of defence; output encoding and input validation remain essential.

For directive-by-directive detail, MDN's Content Security Policy guide is the most practical reference.

Key takeaways

  • CSP restricts where scripts and other resources can load from, sharply limiting the damage of XSS.
  • Avoid 'unsafe-inline' for scripts; use nonces with 'strict-dynamic' or hashes instead.
  • Always start in report-only mode and move to enforcement once violations are understood.
  • Set object-src, base-uri and frame-ancestors explicitly.

Need help with this?

Netifi helps businesses around the world with Web Security. Tell us what you are working on.