Content Security Policy (CSP) is the most powerful web security mechanism available to web developers. A well-crafted CSP can block XSS attacks, prevent data exfiltration, stop clickjacking, and reduce the impact of injection vulnerabilities — even ones that slip past your input validation. But CSP is also nuanced: a weak policy gives false confidence, and a poorly implemented policy can break your application in production. This guide walks through building a strict, effective CSP without breaking your site.
Step 1: Start in Report-Only Mode
Never deploy a CSP directly to production without first testing it. The Content-Security-Policy-Report-Only header sends policy violation reports to a configured endpoint without actually blocking anything. Deploy this first:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Create a simple endpoint to collect the reports:
@app.route('/csp-report', methods=['POST'])
def csp_report():
report = request.get_json(force=True, silent=True) or {}
app.logger.warning("CSP violation: %s", report)
return '', 204
Run your application normally for a few days. The reports will show you exactly which resources are being blocked by the policy so you can adjust it before enforcement begins.
Step 2: Understand the Key Directives
default-src
The fallback for all resource types that do not have their own directive. default-src 'self' means: unless a more specific directive says otherwise, only load resources from your own origin.
script-src
Controls which JavaScript can execute. This is the most important directive for XSS prevention. Common values:
'self'— only scripts from your own origin'nonce-{random}'— only inline scripts carrying this specific nonce value'strict-dynamic'— trust scripts loaded by already-trusted scripts (works with nonces)https://cdn.example.com— scripts from a specific CDN'unsafe-inline'— allows all inline scripts (eliminates XSS protection — avoid)'unsafe-eval'— allows eval() and similar dynamic code execution (avoid)
style-src
Controls CSS loading. Inline styles are common in web applications, making this directive harder to restrict. Consider 'unsafe-inline' for styles (it does not enable JavaScript injection) but prefer nonce-based or hash-based approaches for sensitive applications.
img-src
Controls image loading. A common policy: img-src 'self' data: https: — allows your own images, data URIs for inline images, and any HTTPS image source.
connect-src
Controls which URLs can be reached by fetch, XHR, WebSockets, and EventSource. This prevents exfiltration via JavaScript network requests to attacker-controlled endpoints.
frame-ancestors
Controls which origins can embed your page in an iframe. As discussed in our clickjacking article, this is the modern replacement for X-Frame-Options.
object-src
Controls plugins (Flash, Java applets). Should always be 'none' — there is no legitimate reason for a modern web application to load browser plugins.
base-uri
Controls what values the <base> element can use. Set to 'self' to prevent an injected <base href="https://attacker.com/"> from hijacking all relative URLs on your page.
Step 3: The Nonce Pattern
The most effective approach for script-src uses per-request nonces. A nonce is a random value generated for each response. Scripts execute only if they carry the matching nonce in their nonce attribute. Injected scripts have no nonce and are blocked.
# Generate in Python
import secrets
nonce = secrets.token_urlsafe(16)
# Pass to template
render_template('page.html', csp_nonce=nonce)
# Set the header
response.headers['Content-Security-Policy'] = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}' 'strict-dynamic'; "
f"style-src 'self' 'unsafe-inline'; "
f"img-src 'self' data: https:; "
f"object-src 'none'; "
f"base-uri 'self'; "
f"frame-ancestors 'none'"
)
<!-- In your template -->
<script nonce="{{ csp_nonce }}">
// Your inline JavaScript here
</script>
<script nonce="{{ csp_nonce }}" src="/static/js/app.js"></script>
The 'strict-dynamic' directive extends trust to scripts loaded by your nonced scripts — this means dynamic imports and script loaders (React, webpack chunks) work without listing every URL explicitly.
Step 4: Deploy and Iterate
A production CSP is not a one-time configuration — it requires maintenance. Every time you add a new third-party script, new CDN resource, or new inline script, you need to update the policy. A workflow that works:
- Keep
Content-Security-Policy-Report-Onlyactive alongside the enforcing header during changes - Route CSP reports to a logging service and review them on a regular cadence
- Alert on sudden spikes in violation reports — they may indicate an injection attempt
- Never add
'unsafe-inline'or'unsafe-eval'as a quick fix for a broken CSP — find the specific script and give it a nonce or hash instead
Common Mistakes That Weaken CSP
- Wildcard sources:
script-src https:orscript-src *allows scripts from any HTTPS URL — this eliminates XSS protection entirely - Allowlisting entire CDN origins:
script-src https://cdn.jsdelivr.netallows any script hosted on that CDN, including attacker-uploaded ones - Using 'unsafe-inline' with a nonce: If both are present,
'unsafe-inline'takes precedence in older browsers — the nonce provides no protection - Missing object-src: Without
object-src 'none', Flash and Java applets are allowed by default-src fallback - Not setting base-uri: Allows injected
<base>tags to redirect all relative URLs
Evaluating Your Policy
Google's CSP Evaluator (csp-evaluator.withgoogle.com) analyzes a CSP and reports known bypasses and weaknesses. Paste your policy there before deploying. A policy that passes CSP Evaluator and uses nonces is significantly stronger than one based on URL allowlists.