Content Security Policy (CSP): Implementation Guide

Type: Software Reference Confidence: 0.95 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

CSP Directives by Category

#DirectiveCategoryControlsDefault Fallback
1default-srcFetchFallback for all other fetch directivesNone (browser default)
2script-srcFetchJavaScript and WebAssembly resourcesdefault-src
3style-srcFetchCSS stylesheetsdefault-src
4img-srcFetchImages and faviconsdefault-src
5connect-srcFetchXHR, Fetch, WebSocket, EventSourcedefault-src
6font-srcFetchWeb fonts via @font-facedefault-src
7frame-srcFetch<iframe> and <frame> sourceschild-src then default-src
8media-srcFetch<audio>, <video>, <track> elementsdefault-src
9object-srcFetch<object>, <embed> pluginsdefault-src
10worker-srcFetchWorker, SharedWorker, ServiceWorkerchild-src then script-src then default-src
11base-uriDocumentURLs for <base> elementNo fallback (allows any)
12form-actionNavigationForm submission target URLsNo fallback (allows any)
13frame-ancestorsNavigationWho can embed this page (replaces X-Frame-Options)No fallback (allows any)
14upgrade-insecure-requestsDocumentAuto-upgrade HTTP to HTTPSDisabled
15report-toReportingWhere to send violation reports (replaces report-uri)No reporting

Source Expression Keywords

KeywordMeaningUse In
'self'Same origin onlyAny directive
'none'Block all resources of this typeAny directive
'unsafe-inline'Allow inline <script> and <style> (dangerous)script-src, style-src
'unsafe-eval'Allow eval(), new Function(), setTimeout(string)script-src
'nonce-{random}'Allow elements with matching nonce attributescript-src, style-src
'sha256-{hash}'Allow elements matching the hashscript-src, style-src
'strict-dynamic'Trust scripts loaded by already-trusted scriptsscript-src
'unsafe-hashes'Allow inline event handlers matching hashesscript-src
https:Allow any HTTPS originAny directive
data:Allow data: URIsAny directive

Decision Tree

START: What is your CSP deployment scenario?
├── New application (no existing inline scripts)?
│   ├── YES → Deploy strict nonce-based CSP immediately (see Step 1)
│   └── NO ↓
├── Existing app with inline scripts you control?
│   ├── YES → Add nonces to all inline scripts, then deploy strict CSP (see Step 2)
│   └── NO ↓
├── Existing app with third-party inline scripts?
│   ├── YES → Use hash-based CSP for static scripts + nonces for dynamic (see Step 3)
│   └── NO ↓
├── Legacy app that cannot be modified?
│   ├── YES → Deploy allowlist-based CSP as interim measure, plan migration to strict CSP
│   └── NO ↓
├── Need to prevent clickjacking only?
│   ├── YES → Use frame-ancestors 'none' or 'self' (Step 5)
│   └── NO ↓
└── DEFAULT → Start with report-only mode to audit current resource loading (Step 1)

Step-by-Step Guide

1. Deploy CSP in report-only mode

Start by deploying a strict policy in report-only mode to discover what would break without actually blocking anything. [src6]

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self';
  object-src 'none';
  base-uri 'none';
  report-to csp-endpoint;

Verify: Open browser DevTools > Console -- CSP violations appear as warnings with [Report Only] prefix.

2. Add nonces to all inline scripts

Generate a cryptographically random nonce per request and add it to every inline <script> tag. [src5]

Content-Security-Policy: script-src 'nonce-dGhpcyBpcyBhIG5vbmNl' 'strict-dynamic'

Verify: Inline scripts without a nonce should be blocked. Check console for Refused to execute inline script errors.

3. Build a strict CSP policy

Combine the essential directives for a production-ready strict CSP. [src3]

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

Verify: Run your CSP through Google's CSP Evaluator at https://csp-evaluator.withgoogle.com/.

4. Switch from report-only to enforcement

After monitoring report-only mode for at least 1-2 weeks with no unexpected violations, switch to enforcement. [src6]

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

Verify: Should return the enforced policy (not report-only).

5. Configure frame-ancestors for clickjacking protection

Replace the legacy X-Frame-Options header with CSP frame-ancestors. [src1]

# Prevent all framing
Content-Security-Policy: frame-ancestors 'none';

# Allow same-origin framing only
Content-Security-Policy: frame-ancestors 'self';

# Allow specific origins
Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com;

Verify: Try embedding your page in an <iframe> on a different origin -- it should be blocked.

Code Examples

Express.js/Node.js: Helmet CSP with Per-Request Nonces

const express = require('express');     // ^4.18.0
const helmet = require('helmet');       // ^8.0.0
const crypto = require('crypto');
const app = express();

// Generate per-request nonce middleware
app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

// Configure CSP via Helmet
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: [
        (req, res) => `'nonce-${res.locals.cspNonce}'`,
        "'strict-dynamic'"
      ],
      styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
      imgSrc: ["'self'", "https:"],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      baseUri: ["'none'"],
      formAction: ["'self'"],
      frameAncestors: ["'none'"],
      upgradeInsecureRequests: []
    }
  }
}));
// In templates: <script nonce="<%= cspNonce %>">...</script>

Django: django-csp Configuration

# settings.py -- using django-csp >= 4.0
# pip install django-csp
MIDDLEWARE = ["csp.middleware.CSPMiddleware", ...]

CONTENT_SECURITY_POLICY = {
    "DIRECTIVES": {
        "default-src": ["'self'"],
        "script-src": ["'strict-dynamic'"],  # nonces added automatically
        "style-src": ["'self'"],
        "img-src": ["'self'", "https:"],
        "connect-src": ["'self'"],
        "font-src": ["'self'"],
        "object-src": ["'none'"],
        "base-uri": ["'none'"],
        "form-action": ["'self'"],
        "frame-ancestors": ["'none'"],
        "upgrade-insecure-requests": True,
    }
}
# In templates: {% load csp %} <script nonce="{% csp_nonce %}">...</script>

Nginx: CSP Header Configuration

# /etc/nginx/snippets/csp-headers.conf
# Hash-based CSP for static sites (Nginx cannot generate nonces natively)
add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' 'sha256-{HASH_OF_INLINE_SCRIPT}';
  style-src 'self';
  img-src 'self' https:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'none';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
" always;

Apache: CSP Header Configuration

# .htaccess or httpd.conf
Header always set Content-Security-Policy "\
  default-src 'self'; \
  script-src 'self' 'sha256-{HASH_OF_INLINE_SCRIPT}'; \
  style-src 'self'; \
  img-src 'self' https:; \
  font-src 'self'; \
  connect-src 'self'; \
  object-src 'none'; \
  base-uri 'none'; \
  form-action 'self'; \
  frame-ancestors 'none'; \
  upgrade-insecure-requests"

Anti-Patterns

Wrong: Using unsafe-inline with script-src

# BAD -- unsafe-inline completely negates CSP's XSS protection
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
# Any XSS payload can execute inline scripts freely

Correct: Nonce-based script-src

# GOOD -- only scripts with the correct nonce execute
Content-Security-Policy: script-src 'nonce-abc123def456' 'strict-dynamic'
# XSS payloads cannot guess the per-request nonce

Wrong: Wildcard sources in script-src

# BAD -- allows scripts from ANY origin
Content-Security-Policy: default-src *; script-src *

Correct: Explicit origin allowlist or nonces

# GOOD -- use nonce-based CSP with strict-dynamic
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'

Wrong: Overly permissive default-src

# BAD -- default-src https: allows loading from ANY HTTPS origin
Content-Security-Policy: default-src https:
# Attackers can host payloads on any HTTPS domain they control

Correct: Restrictive default-src with explicit overrides

# GOOD -- lock down default, open specific directives as needed
Content-Security-Policy: default-src 'none'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; style-src 'self'; img-src 'self' https:; font-src 'self'; connect-src 'self'

Wrong: CSP via meta tag for frame-ancestors

<!-- BAD -- frame-ancestors, report-uri, report-to, and sandbox
     are IGNORED in meta tags -->
<meta http-equiv="Content-Security-Policy"
      content="frame-ancestors 'none'; report-uri /csp-report">

Correct: CSP via HTTP header

# GOOD -- all directives work via HTTP header
Content-Security-Policy: frame-ancestors 'none'; report-to csp-endpoint

Wrong: Using unsafe-eval for convenience

# BAD -- allows eval(), new Function(), setTimeout(string)
Content-Security-Policy: script-src 'self' 'unsafe-eval'

Correct: Refactoring to avoid eval

// GOOD -- replace eval() with safe alternatives
// Instead of: setTimeout('doSomething()', 1000)
setTimeout(doSomething, 1000);  // pass function reference
// Instead of: new Function('return ' + userInput)
JSON.parse(userInput);  // for data parsing

Common Pitfalls

Diagnostic Commands

# Check current CSP headers of a live site
curl -sI https://your-site.com | grep -i content-security-policy

# Check CSP with full headers
curl -sI https://your-site.com | grep -iE '(content-security|report)'

# Generate SHA-256 hash of an inline script for hash-based CSP
echo -n 'console.log("hello")' | openssl dgst -sha256 -binary | openssl base64

# Validate CSP: visit https://csp-evaluator.withgoogle.com/

# Scan for inline scripts that need nonces
grep -rn '<script>' --include="*.html" --include="*.ejs" .

# Scan for inline event handlers that CSP will block
grep -rn 'onclick=\|onload=\|onerror=\|onsubmit=' --include="*.html" .

Version History & Compatibility

CSP LevelStatusBrowser SupportKey Features
CSP Level 3W3C Working Draft (June 2025)Chrome 59+, Firefox 58+, Safari 15.4+, Edge 79+strict-dynamic, report-to, worker-src, manifest-src, nonce/hash enhancements
CSP Level 2W3C RecommendationChrome 40+, Firefox 31+, Safari 10+, Edge 15+script-src, style-src, base-uri, form-action, frame-ancestors, report-uri, nonce/hash
CSP Level 1W3C Recommendation (deprecated)Chrome 25+, Firefox 23+, Safari 7+, Edge 12+Basic default-src, script-src, img-src, style-src, connect-src
Trusted TypesW3C DraftChrome 83+, Firefox behind flagrequire-trusted-types-for 'script', DOM sink protection
Reporting API v1W3C DraftChrome 96+, Edge 96+report-to with Reporting-Endpoints header

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Any web application serving HTML to browsersStatic file server with no HTML (pure API)CORS headers for API protection
Need XSS defense-in-depth beyond output encodingCSP is your only XSS defenseOutput encoding + CSP together
Controlling third-party script loading (analytics, ads)You trust all content from all originsStill use CSP -- trust boundaries change
Preventing clickjacking (frame-ancestors)Only need clickjacking protectionX-Frame-Options as legacy fallback, but prefer CSP
Compliance requires CSP (PCI DSS, SOC 2)Internal-only tool with no external accessCSP still recommended but lower priority

Important Caveats

Related Units