How to Set Up Content Security Policy (CSP)

How-To Guide

How to Set Up Content Security Policy (CSP)

The most powerful security header. And the easiest to misconfigure.

TL;DR

CSP prevents XSS by controlling which resources can load. Start with Content-Security-Policy-Report-Only so you can test without breaking anything. Your baseline is default-src 'self'. Add nonces for inline scripts. List third-party domains only where you actually need them. Monitor violations before you enforce.

Prerequisites

  • Ability to set HTTP headers (via server config, middleware, or hosting platform)
  • Understanding of what resources your app loads (scripts, styles, images, fonts)
  • Access to browser DevTools for testing
  • Optional: A reporting endpoint for CSP violations

What is Content Security Policy?

CSP is an HTTP header that tells browsers which content sources are trusted. When a resource violates the policy, the browser blocks it and optionally reports the violation. That's really the whole idea.

Why CSP Matters

Even if an attacker injects malicious code into your page (via XSS or a compromised dependency), CSP can prevent it from executing. The header checks whether the resource matches your policy before the browser runs it.

CSP Directive Reference

DirectiveControlsExample
default-srcFallback for all resource types'self'
script-srcJavaScript sources'self' 'nonce-abc'
style-srcCSS sources'self' 'unsafe-inline'
img-srcImage sources'self' data: https:
font-srcFont sources'self' https://fonts.gstatic.com
connect-srcXHR, WebSocket, fetch() URLs'self' https://api.example.com
frame-srciframe sources'self' https://youtube.com
frame-ancestorsWho can embed this page'none' or 'self'
object-srcFlash, plugins'none'
base-uri
<base></base>
tag URLs
'self'
form-actionForm submission URLs'self'

Step-by-Step Implementation

1

Start with Report-Only Mode

Use Content-Security-Policy-Report-Only to test without breaking your site:

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

This logs violations to the console and your reporting endpoint without blocking anything.

2

Build Your Base Policy

Lock it down first, then open only what you need:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self';
  font-src 'self';
  connect-src 'self';
  frame-src 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
3

Add Third-Party Resources

Identify and allow external resources your app needs:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net https://www.googletagmanager.com;
  style-src 'self' https://fonts.googleapis.com;
  img-src 'self' data: https: blob:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com https://analytics.example.com;
  frame-src https://www.youtube.com https://player.vimeo.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
4

Implement Nonces for Inline Scripts

Instead of using 'unsafe-inline', use nonces:

Generate a unique nonce for each request:

// Express.js middleware
const crypto = require('crypto');

app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use((req, res, next) => {
  const nonce = res.locals.nonce;
  res.setHeader('Content-Security-Policy',
    `default-src 'self'; ` +
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; ` +
    `style-src 'self' 'nonce-${nonce}';`
  );
  next();
});

Add nonce to your inline scripts:

<!-- In your HTML template -->
<script nonce="<%= nonce %>">
  // This inline script will execute
  console.log('Authorized inline script');
</script>

<script>
  // This will be BLOCKED - no nonce
  console.log('Unauthorized script');
</script>
5

Use Hashes for Static Inline Scripts (Alternative)

For static inline scripts that don't change, use hashes:

# Generate hash of your inline script
echo -n "console.log('Hello');" | openssl dgst -sha256 -binary | openssl base64
# Output: 5jFwrAK0UV47oFbVg/iCCBbxD8X1w+QvoOUepu4C2YA=

Add the hash to your CSP:

Content-Security-Policy: script-src 'self' 'sha256-5jFwrAK0UV47oFbVg/iCCBbxD8X1w+QvoOUepu4C2YA=';
6

Set Up Violation Reporting

Create an endpoint to receive CSP violation reports:

// Express.js endpoint for CSP reports
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report'];
  console.log('CSP Violation:', {
    blockedUri: report['blocked-uri'],
    violatedDirective: report['violated-directive'],
    documentUri: report['document-uri'],
    sourceFile: report['source-file'],
    lineNumber: report['line-number'],
  });

  // Send to your logging service
  // await logService.log('csp-violation', report);

  res.status(204).end();
});

Add report-uri to your CSP:

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

Modern reporting with report-to:

# Headers
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"/csp-report"}]}
Content-Security-Policy: default-src 'self'; report-to csp-endpoint;
7

Switch to Enforced Mode

Once violations are resolved, remove -Report-Only from the header:

# Before (testing)
Content-Security-Policy-Report-Only: default-src 'self'; ...

# After (enforced)
Content-Security-Policy: default-src 'self'; ...

CSP Security Checklist

  • Never use 'unsafe-inline' for script-src if possible - use nonces instead
  • Avoid 'unsafe-eval' unless absolutely required (some frameworks need it)
  • Always include object-src 'none' to block Flash/plugins
  • Use frame-ancestors 'none' or 'self' to prevent clickjacking
  • Include base-uri 'self' to prevent base tag hijacking
  • Test thoroughly in report-only mode before enforcing
  • Monitor violation reports after deployment

Common CSP Configurations

Strict CSP for Modern Apps

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{random}' 'strict-dynamic';
  style-src 'self' 'nonce-{random}';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.yoursite.com;
  frame-src 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;

Next.js / React Application

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-eval' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: blob: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' wss: https:;
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'self';

Note: 'unsafe-eval' is needed for development hot reloading. Consider removing in production.

Static Marketing Site

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://www.google-analytics.com;
  frame-src https://www.youtube.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self' https://formsubmit.co;
  frame-ancestors 'none';
  upgrade-insecure-requests;

E-commerce with Payment Integration

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://js.stripe.com https://www.paypal.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https: blob:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.stripe.com https://api.yoursite.com;
  frame-src https://js.stripe.com https://www.paypal.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self' https://api.stripe.com;
  frame-ancestors 'none';

How to Verify It Worked

Method 1: Browser Console

  1. Open DevTools (F12)
  2. Go to Console tab
  3. Look for CSP violation errors (red text mentioning "Refused to...")
  4. Each violation shows what was blocked and why

Method 2: CSP Evaluator

Use Google's CSP Evaluator to analyze your policy for weaknesses.

Method 3: Security Headers Scanner

# Check your CSP header
curl -I https://yoursite.com | grep -i content-security-policy

Method 4: Test XSS is Blocked

Try injecting a script via URL parameters (if your app reflects user input):

# This should be blocked by CSP
https://yoursite.com/search?q=<script>alert('xss')</script>

Common Errors and Troubleshooting

Inline scripts blocked

  • Solution A: Add nonces to inline scripts
  • Solution B: Move inline scripts to external files
  • Solution C: Use 'unsafe-inline' (weakens security)

Third-party scripts blocked

  • Identify the domain: Check console for the blocked URL
  • Add to script-src: script-src 'self' https://cdn.example.com
  • Check subdomains: You might need https://*.example.com

Styles not loading

  • Inline styles: Add 'unsafe-inline' to style-src or use nonces
  • External stylesheets: Add the stylesheet domain to style-src
  • CSS-in-JS: Many libraries require 'unsafe-inline'

Images not loading

  • Base64 images: Add data: to img-src
  • External images: Add https: or specific domains
  • Blob URLs: Add blob: to img-src

API calls blocked

  • Fetch/XHR: Add API domain to connect-src
  • WebSockets: Add wss: and WebSocket URL to connect-src

Pro Tip: Iterative CSP Development

Start permissive and tighten gradually. Report-only mode shows you exactly what needs to be allowed before you break anything. Once violations drop to zero, start removing 'unsafe-inline' and 'unsafe-eval' one directive at a time.

Frequently Asked Questions

What is Content Security Policy (CSP)?

CSP is an HTTP header that tells browsers which content sources are allowed on your page. It prevents XSS by blocking unauthorized scripts, styles, images, and other resources from loading or executing.

Why does my CSP break my website?

CSP blocks anything that doesn't match your policy. Common culprits: inline scripts (add nonces), third-party resources (add their domains), or eval() calls (add 'unsafe-eval'). Open your browser console and it'll tell you exactly what's being blocked and why.

Should I use 'unsafe-inline' in my CSP?

'unsafe-inline' significantly weakens XSS protection. Use nonces or hashes for inline scripts instead. For styles, it's often unavoidable with CSS-in-JS libraries, but migrate to external stylesheets when you can.

What is a CSP nonce and how does it work?

A nonce is a random, one-time value generated for each request. Add it to your CSP header (script-src 'nonce-abc123') and to each authorized inline script tag (nonce="abc123"). Scripts without a matching nonce don't execute.

What is the difference between report-only and enforced mode?

Content-Security-Policy blocks violations. Content-Security-Policy-Report-Only only reports them without blocking. Start with report-only to find what's broken, then switch to enforced once your policy's clean.

Test Your CSP Configuration

Scan your site to check if CSP is configured correctly and identify potential weaknesses.

How-To Guides

How to Set Up Content Security Policy (CSP)