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 everythinghttps://cdn.example.com— a specific domain*.example.com— any subdomain'unsafe-inline'— allow inline scripts/styles (weakens protection significantly)'unsafe-eval'— alloweval()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.
See Also
- Python Api Key Management Why apps use special passwords called API keys, and how to keep them safe — explained with a library card analogy
- Python Attribute Based Access Control How apps make fine-grained permission decisions based on who you are, what you're accessing, and the circumstances — explained with an airport analogy
- Python Audit Logging Learn Audit Logging with a clear mental model so your Python code is easier to trust and maintain.
- Python Bandit Security Scanning Why Bandit Security Scanning helps Python teams catch painful mistakes early without slowing daily development.
- Python Clickjacking Prevention How invisible website layers trick you into clicking the wrong thing, and how Python apps stop it