Implementing Content Security Policy (CSP) Headers in Flask to Prevent XSS
Content Security Policy (CSP) headers instruct browsers to block unauthorized resources like scripts and styles, helping prevent cross-site scripting (XSS) attacks in Flask applications. OWASP ranks XSS highly; CSP enforces whitelists for approved sources. We will implement CSP using Flask’s @after_request decorator, nonces for inline content, and report-only mode for safe testing.
Why CSP in Flask Apps?
Cross-site scripting (XSS) remains among the top web security risks in the OWASP Top 10. Attackers inject code like <script>stealCookies()</script> into pages viewed by victims. CSP addresses this by letting us whitelist approved sources for scripts, styles, images, and connections.\n\nCSP evolved from early proposals around 2009, standardized in 2012 (CSP Level 1), with later versions adding nonces, hashes, and stricter controls.\n\nKey directives include:\n\n| Directive | Controls | Example |\n|-----------|----------|---------|\n| default-src 'self' | All resources | Blocks external assets by default |\n| script-src 'self' 'nonce-xyz' | Scripts | Allows self and specific inline with nonce |\n| style-src 'self' 'unsafe-inline' | Styles | Permits inline styles (use cautiously) |\n| img-src 'self' data: | Images | Allows base64 data URIs |\n| connect-src 'self' | API/fetch | Prevents data exfiltration |\n\nFlask does not set CSP headers by default, so applications rely solely on escaping—which can miss cases. CSP adds defense in depth.
Step 1: Implementing a Basic CSP Policy
from flask import Flask, after_this_request, make_response
app = Flask(__name__)
@app.after_request
def add_csp(response):
response.headers['Content-Security-Policy'] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self'; "
"img-src 'self' data:; "
"connect-src 'self';"
)
return response
@app.route('/')
def index():
return "<script>alert('XSS?')</script><h1>Hello</h1>"
Run flask run and load the root route in your browser. Open DevTools Console: expect “Refused to execute inline script because it violates the following Content Security Policy directive…”. Network tab shows the CSP header.
Step 2: Using Nonces for Inline Scripts and Styles
To safely include inline scripts and styles—which are common but risky—we generate a cryptographically secure nonce (number used once) before each request. The browser only executes inline content matching this nonce. This is more secure than 'unsafe-inline', which permits all inline code and remains vulnerable to injection, though it requires propagating the nonce to your templates.
import secrets
from flask import g
@app.before_request
def add_nonce():
g.csp_nonce = secrets.token_urlsafe(16)
@app.after_request
def add_csp(response):
nonce = g.get('csp_nonce', '')
policy = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}'; "
f"style-src 'self' 'nonce-{nonce}'; "
"img-src 'self' data:; "
"connect-src 'self';"
)
response.headers['Content-Security-Policy'] = policy
return response
@app.route('/')
def index():
nonce = g.csp_nonce
return f"""
<script nonce="{nonce}">console.log('Safe inline');</script>
<style nonce="{nonce}">h1 {{ color: red; }}</style>
<h1>Hello CSP</h1>
"""
The permitted inline content executes safely. Try adding <script>alert(1)</script> without a nonce—it will be blocked.
Step 3: Report-Only Mode for Testing
Before enforcing blocks, test with Content-Security-Policy-Report-Only. It logs violations to console (and report endpoint if configured) without preventing loads. This reveals issues gradually. Switch to full CSP once tuned. Trade-off: no immediate protection during testing.
response.headers['Content-Security-Policy-Report-Only'] = policy
response.headers['Report-To'] = '{"group":"csp","max_age":10886400,"endpoints":[{"url":"https://your-report-endpoint.com"}]}'
Prod: Switch to enforcing.
Step 4: Production Configurations and Extensions
Config-driven:
app.config['CSP_DEFAULT_SRC'] = "'self'"
app.config['CSP_SCRIPT_SRC'] = "'self' 'nonce-%s'"
# etc.
def build_policy():
return '; '.join([f"{k.replace('CSP_','')[:-4].lower()}-src {v % g.csp_nonce if '%s' in v else v}"
for k,v in app.config.items() if k.startswith('CSP_')])
Flask-Talisman alt (pip install flask-talisman):
from flask_talisman import Talisman
Talisman(app, content_security_policy={
'default-src': "'self'",
'script-src': ["'self'", "'nonce-{{csp_nonce}}'"]
})
The manual approach avoids extra dependencies, giving you full control.
Common Violations & Fixes
| Violation | Console Log | Fix |
|---|---|---|
| Refused inline script | Refused to execute inline script | Add nonce |
| External CDN | Refused to load script from https://evil.com | 'self' https://trusted.cdn.com |
| Data URI img | Refused img-src | img-src data: https: |
| AJAX leak | Refused connect-src | connect-src 'self' https://api.com |
| Font WOFF | Refused font-src | font-src 'self' data: |
Audit: Chrome DevTools > Console > CSP.
Verify: Payload Tests
# Vuln test
curl -I http://localhost:5000/ | grep CSP
# Inject XSS
curl "http://localhost:5000/?q=<script>alert(1)</script>" | grep -i script # Escaped/blocked
Checklist:
- Nonce unique/secure
- Report-only → enforce
- No
'unsafe-inline' - OWASP ZAP/Chrome audit passes
Combined with proper input escaping and validation, CSP provides strong protection against XSS in Flask applications.
Related: 34. Flask Startup, 33. Flask-SQLAlchemy, 28. Flask OWASP
Sponsored by Durable Programming
Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.
Hire Durable Programming