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:

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:

  1. Keep Content-Security-Policy-Report-Only active alongside the enforcing header during changes
  2. Route CSP reports to a logging service and review them on a regular cadence
  3. Alert on sudden spikes in violation reports — they may indicate an injection attempt
  4. 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

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.