Security & Authentication

Content Security Policy (CSP): A Complete Implementation Guide

How to write, deploy, and iterate on Content Security Policy headers to prevent XSS, data injection, and clickjacking attacks.

What Is Content Security Policy?

Content Security Policy (CSP) is an HTTP response header that tells the browser which resources are allowed to load on a page. It is the most powerful defense against Cross-Site Scripting (XSS) — attacks where malicious scripts are injected into your page and run in the victim's browser.

Without CSP, any JavaScript that ends up on your page — whether injected through an XSS flaw, a compromised CDN, or an evil browser extension — can access cookies, read the DOM, make network requests, and exfiltrate data.

CSP breaks this by creating an explicit allowlist. The browser refuses to execute any script, load any image, or make any network request that isn't on the list.

Header Syntax

Content-Security-Policy: directive1 source1 source2; directive2 source3;

Each directive controls a resource type. Each source defines where that resource can come from. Semicolons separate directives.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' https://fonts.googleapis.com;
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';

Key Directives

default-src — The Fallback

default-src sets the default policy for any resource type that doesn't have its own directive. Start with default-src 'self' — only allow resources from your own origin — then add specific permissions where needed.

script-src — JavaScript

The most critical directive. It controls which JavaScript can execute.

SourceMeaning
`'self'`Same origin as the page
`https://cdn.example.com`Specific HTTPS host
`'nonce-abc123'`Inline script with matching nonce
`'sha256-<hash>'`Inline script matching a specific hash
`'unsafe-inline'`Allow all inline scripts (disables most XSS protection)
`'unsafe-eval'`Allow eval() and Function() constructors
`'strict-dynamic'`Allow scripts loaded by trusted scripts

Never use 'unsafe-inline' in script-src — it negates the XSS protection that CSP is designed to provide.

style-src — Stylesheets

Controls CSS loading. Unlike scripts, inline <style> tags and style= attributes are less dangerous, but 'unsafe-inline' in style-src still enables CSS injection attacks (data exfiltration via CSS selectors).

img-src — Images

img-src 'self' https: data:;

data: allows inline base64 images. https: allows images from any HTTPS source. This is typically the most permissive directive because image loads are lower risk.

connect-src — Fetch, XHR, WebSocket

Controls which URLs your JavaScript can make network requests to. Critical for preventing data exfiltration:

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

frame-ancestors — Clickjacking Defense

frame-ancestors replaces the old X-Frame-Options header and is more flexible:

frame-ancestors 'none';          # Block all framing
frame-ancestors 'self';          # Only allow same-origin framing
frame-ancestors https://app.example.com;  # Allow specific parent

Deployment Strategy: Report-Only First

Never deploy a CSP in enforcing mode as your first step. You will break things.

Start with Content-Security-Policy-Report-Only. This header sends violation reports but does not block anything:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  report-uri /csp-violations

Build a simple endpoint to collect violations:

# Django view to collect CSP violation reports
import json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import logging

logger = logging.getLogger(__name__)

@csrf_exempt
def csp_violations(request):
    if request.method == 'POST':
        try:
            report = json.loads(request.body)
            logger.info('CSP violation', extra=report)
        except json.JSONDecodeError:
            pass
    return HttpResponse(status=204)

Collect violations for 1-2 weeks across all your pages. Then refine your policy to add legitimate sources before switching to the enforcing header.

Handling Inline Scripts

Inline scripts — <script> tags with code directly inside, and onclick= attributes — are the primary vector for XSS. CSP's power against XSS comes from blocking inline scripts. But many sites rely on them heavily.

Nonce-Based Approach

A nonce (number used once) is a random token generated per request. You add it to your CSP header and to each trusted inline script. The browser only executes inline scripts whose nonce matches the header.

# Django middleware: generate nonce per request
import secrets
from django.utils.decorators import method_decorator

class CSPNonceMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request.csp_nonce = secrets.token_hex(16)
        response = self.get_response(request)
        response['Content-Security-Policy'] = (
            f"default-src 'self'; "
            f"script-src 'self' 'nonce-{request.csp_nonce}' 'strict-dynamic';"
        )
        return response
<!-- Template: use the nonce on your inline scripts -->
<script nonce="{{ request.csp_nonce }}">
  initApp();
</script>

An attacker who injects <script>alert(1)</script> cannot know the nonce for this request — their script won't execute.

Hash-Based Approach

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

# Generate SHA-256 hash of inline script content
echo -n 'initApp();' | openssl dgst -sha256 -binary | openssl base64
# Output: <hash>
Content-Security-Policy: script-src 'self' 'sha256-<hash>'

The browser computes the hash of each inline script and only executes ones that match a hash in the CSP header.

strict-dynamic

'strict-dynamic' propagates trust: a script allowed by nonce or hash can load additional scripts dynamically. This enables modern frameworks (React, Vue) that inject scripts at runtime, without needing to allowlist every chunk URL.

Common Mistakes

Mistake 1: Wildcard sources

script-src *;  # Allows scripts from anywhere — useless CSP

Mistake 2: unsafe-inline with hashes

When you add a hash or nonce, 'unsafe-inline' is automatically ignored by modern browsers — but it's still bad practice and confusing.

Mistake 3: Forgetting connect-src

A strict script-src that blocks XSS injection is undermined if connect-src * allows injected scripts to exfiltrate data to any server.

Mistake 4: CSP for SPAs without strict-dynamic

Single-page applications typically inject <script> tags via module bundlers. Without 'strict-dynamic', you'd need to allowlist every chunk hash. Use 'nonce-...' 'strict-dynamic' instead.

Related Protocols

Related Glossary Terms

More in Security & Authentication