Content Security Policy in Python — Core Concepts

What CSP protects against

Content Security Policy is a browser-enforced defense against injection attacks, primarily cross-site scripting (XSS). Even if an attacker manages to inject a <script> tag into your page, CSP prevents the browser from executing it unless the script source is explicitly allowed.

CSP also mitigates data exfiltration (injected code can’t send data to unauthorized domains), clickjacking (via frame-ancestors), and mixed content (loading HTTP resources on HTTPS pages).

How CSP works

The server sends a Content-Security-Policy HTTP header with every response. This header contains directives that define what the browser is allowed to load:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *; frame-ancestors 'none'

The browser reads these rules and enforces them. If the page tries to load a script from https://evil.com, the browser blocks it and logs a violation.

Key directives

default-src — the fallback for any directive not explicitly set. Starting with default-src 'self' means only your own domain is allowed unless you specify otherwise.

script-src — which sources can provide JavaScript. This is the most critical directive for XSS prevention.

style-src — which sources can provide CSS.

img-src — which sources can provide images.

connect-src — which URLs JavaScript can connect to (fetch, XMLHttpRequest, WebSocket).

font-src — which sources can provide fonts.

frame-ancestors — which sites can embed your page in an iframe (replaces X-Frame-Options).

report-uri / report-to — where to send violation reports so you can monitor attacks without breaking anything.

Source values

Each directive accepts a list of sources:

  • 'self' — same origin as the page
  • 'none' — block everything
  • https://cdn.example.com — a specific domain
  • *.example.com — any subdomain
  • 'unsafe-inline' — allow inline scripts/styles (weakens protection significantly)
  • 'unsafe-eval' — allow eval() and similar dynamic code execution
  • 'nonce-abc123' — allow specific inline scripts tagged with a matching nonce
  • 'sha256-...' — allow a specific inline script by its hash

Adding CSP in Python frameworks

Django with django-csp:

# settings.py
MIDDLEWARE = [
    "csp.middleware.CSPMiddleware",
    # ...
]

CONTENT_SECURITY_POLICY = {
    "DIRECTIVES": {
        "default-src": ["'self'"],
        "script-src": ["'self'", "https://cdn.example.com"],
        "style-src": ["'self'", "'unsafe-inline'"],
        "img-src": ["'self'", "https:", "data:"],
        "connect-src": ["'self'", "https://api.example.com"],
        "frame-ancestors": ["'none'"],
    }
}

Flask with Flask-Talisman:

from flask_talisman import Talisman

csp = {
    "default-src": "'self'",
    "script-src": "'self' https://cdn.example.com",
    "style-src": "'self' 'unsafe-inline'",
}
Talisman(app, content_security_policy=csp)

FastAPI with custom middleware or secure library.

Report-only mode: the safe way to start

Deploying a strict CSP without testing will likely break your site — third-party analytics, embedded widgets, inline scripts all need to be accounted for. Start with report-only mode:

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

The browser logs violations but doesn’t block anything. Collect reports, fix violations, and once clean, switch to enforcing mode.

Common misconception

Many developers think CSP replaces the need for output escaping and XSS prevention in code. It doesn’t. CSP is a defense-in-depth layer. Your primary defense is still proper output escaping (template auto-escaping in Django/Jinja2) and input validation. CSP catches what slips through — it’s the safety net, not the first line of defense.

If you rely solely on CSP and later need to add 'unsafe-inline' for a third-party library, your entire XSS defense collapses.

The unsafe-inline problem

Many sites need inline styles for their CSS framework or legacy code. Adding 'unsafe-inline' to style-src is a common and relatively low-risk compromise. Adding it to script-src is far more dangerous — it allows any injected <script> tag to execute.

The alternative is nonces: generate a unique random value for each page load, include it in the CSP header and on each legitimate inline script tag. The attacker can’t predict the nonce, so their injected scripts won’t have it and won’t execute.

The one thing to remember: Start with Content-Security-Policy-Report-Only to find what breaks, fix violations gradually, then enforce — and use nonces instead of 'unsafe-inline' for scripts to maintain real XSS protection.

pythonsecurityweb

See Also