Content Security Policy, or CSP, is one of the most effective browser-side defenses against script injection and many cross-site scripting attacks. In Django 6.0, CSP support is built into the framework through ContentSecurityPolicyMiddleware, SECURE_CSP, SECURE_CSP_REPORT_ONLY, and nonce support via the csp() context processor. For older Django versions, or for projects already using it, django-csp remains a maintained option, and its current 4.0 release uses a dictionary-based policy format.

This guide uses the safest practical approach: start with report-only mode, observe what your site needs, then move to enforcement. That matches both Django’s CSP documentation and MDN’s implementation guidance, which recommends testing with Content-Security-Policy-Report-Only before blocking resources and notes that a strict CSP based on nonces or hashes is the recommended model for XSS mitigation.

Step 1: Understand what CSP will do in your Django app

CSP is an HTTP response header that tells the browser which sources are allowed for scripts, styles, images, fonts, frames, and network requests. Django’s documentation describes the two main headers: Content-Security-Policy, which enforces and blocks violating content, and Content-Security-Policy-Report-Only, which only reports violations without blocking.

In practice, this means CSP helps protect your Django site even if a template bug or unsafe output lets attacker-controlled HTML appear in the page. A good CSP can stop that injected content from executing JavaScript, loading hostile remote scripts, or abusing dangerous features like object embeds and altered base URLs. MDN specifically recommends strict policies that rely on nonces or hashes, with object-src 'none' and base-uri 'none' as part of the baseline.

Step 2: Choose the right implementation path for your Django version

If your project runs Django 6.0 or newer, use Django’s built-in CSP features. Django 6.0 introduced native CSP support, including middleware, settings, constants, and template nonce support. That is the most direct and future-facing option.

If your project is on an older Django version, use django-csp. Its documentation states that version 4.0 is current and that configuration is now dictionary-based through CONTENT_SECURITY_POLICY and CONTENT_SECURITY_POLICY_REPORT_ONLY. It also warns that the 4.0 format breaks compatibility with older django-csp settings from 3.8 and earlier.

Step 3: Add built-in CSP to a Django 6+ project

Start by adding Django’s CSP middleware to your MIDDLEWARE list:

MIDDLEWARE = [
    # ...
    "django.middleware.csp.ContentSecurityPolicyMiddleware",
    # ...
]

Django’s official CSP how-to uses exactly this middleware as the first basic configuration step.

Now add an initial report-only policy in settings.py:

from django.utils.csp import CSP

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],
    "img-src": [CSP.SELF, "data:"],
    "style-src": [CSP.SELF],
    "script-src": [CSP.SELF],
    "font-src": [CSP.SELF],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.NONE],
    "frame-ancestors": [CSP.NONE],
    "report-uri": "/csp-report/",
}

This follows Django’s documented model of defining CSP as a Python dictionary and uses Django-provided constants like CSP.SELF. The structure is consistent with Django’s built-in examples and with MDN’s recommendation to disable object-src and base-uri in a strict policy.

This first policy is intentionally conservative but still realistic for many Django apps. default-src 'self' creates a same-origin baseline, while separate directives give you room to open only what is truly needed. frame-ancestors 'none' helps defend against clickjacking by preventing the site from being embedded in frames. report-uri gives you visibility before you enforce.

Step 4: Add django-csp instead if your project is older

Install the package in your environment:

pip install django-csp

Then add its middleware, according to the package documentation for your version. The current docs center configuration in CONTENT_SECURITY_POLICY and CONTENT_SECURITY_POLICY_REPORT_ONLY, while also documenting middleware and nonce support.

A modern django-csp 4.0 report-only configuration looks like this:

CONTENT_SECURITY_POLICY_REPORT_ONLY = {
    "DIRECTIVES": {
        "default-src": ["'self'"],
        "img-src": ["'self'", "data:"],
        "style-src": ["'self'"],
        "script-src": ["'self'"],
        "font-src": ["'self'"],
        "connect-src": ["'self'"],
        "object-src": ["'none'"],
        "base-uri": ["'none'"],
        "frame-ancestors": ["'none'"],
        "report-uri": "/csp-report/",
    }
}

The exact shape is package-specific, so if you use django-csp, follow its current 4.0 documentation rather than older blog posts. That matters because the project explicitly says 4.0 introduced a breaking configuration change from earlier releases.

Step 5: Run your site in report-only mode first

Do not begin by blocking resources on a production site. MDN recommends first using report-only mode, and Django’s built-in CSP features support this directly through SECURE_CSP_REPORT_ONLY. This lets your site keep working while you collect violations and discover missing domains, inline code, or framework behavior that your first draft policy did not account for.

Open your pages, log in, test forms, open admin pages, use search, try any rich editor, and exercise all JavaScript-driven features. CSP tuning is never only about the home page. You want to test every page type that loads static files, external assets, AJAX endpoints, admin widgets, analytics scripts, fonts, or embedded media. MDN notes that practical CSP rollout involves implementing a strict policy and then identifying which resources fail to load so you can address them safely.

Step 6: Add a CSP report endpoint in Django

Create a minimal view to receive violation reports:

# views.py
import json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def csp_report(request):
    if request.method == "POST":
        try:
            payload = json.loads(request.body.decode("utf-8"))
            print("CSP report:", payload)
        except Exception:
            pass
    return HttpResponse(status=204)

Then register the URL:

# urls.py
from django.urls import path
from .views import csp_report

urlpatterns = [
    path("csp-report/", csp_report, name="csp_report"),
]

And make sure your policy points to the same endpoint:

"report-uri": "/csp-report/",

Django’s built-in documentation shows report-uri as part of a report-only policy example, and django-csp also documents a dedicated section for CSP violation reports.

When the browser sends reports, inspect them carefully. Some will reveal legitimate missing sources, but others will show old inline code patterns you should remove rather than allow. The goal is not to silence reports at any cost. The goal is to make the site compatible with a safer policy.

Step 7: Remove inline JavaScript and inline event handlers

One of the biggest reasons CSP rollout fails is that many Django templates contain inline JavaScript and HTML attributes like onclick, onchange, or onload. MDN’s strict CSP guidance explicitly notes that a strict policy disables unsafe inline JavaScript and inline event handlers, because they are a major XSS risk.

Instead of this:

<button onclick="openMenu()">Open</button>
<script>
  initDashboard();
</script>

Refactor to this:

<button id="open-menu-btn">Open</button>
<script src="{% static 'js/dashboard.js' %}"></script>
document.getElementById("open-menu-btn")?.addEventListener("click", openMenu);
initDashboard();

This change is important because it lets you keep script-src strict without falling back to unsafe allowances like 'unsafe-inline'. Django’s nonce support exists for the inline cases you truly cannot avoid, but the cleanest architecture is still to move code into static files whenever possible.

Step 8: Use nonces for the inline scripts you cannot remove

For Django 6+, add the nonce placeholder to script-src or style-src:

from django.utils.csp import CSP

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "style-src": [CSP.SELF, CSP.NONCE],
    "img-src": [CSP.SELF, "data:"],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.NONE],
    "frame-ancestors": [CSP.NONE],
    "report-uri": "/csp-report/",
}

Django’s documentation states that CSP.NONCE applies to script-src and style-src, and that you must also add the csp() context processor to expose csp_nonce in templates.

Add the context processor:

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                # ...
                "django.template.context_processors.csp",
            ],
        },
    },
]

Then use the nonce in templates:

<script nonce="{{ csp_nonce }}">
  window.APP_CONFIG = {
    username: "{{ request.user.username|escapejs }}"
  };
</script>

This is the recommended way to keep a strict policy while permitting specific server-generated inline code. Django documents this exact nonce flow: include CSP.NONCE, add the context processor, and use nonce="{{ csp_nonce }}" in the template.

If you use django-csp, it also documents generated nonce support through middleware and a context processor, so the same overall idea applies even though the configuration syntax differs by package.

Step 9: Do not use 'unsafe-inline' unless you have no choice

It may be tempting to allow inline scripts just to make the site work faster, but that weakens one of the most important protections CSP provides. Django’s own nonce example labels 'unsafe-inline' as the less secure option, and MDN’s strict CSP guidance explicitly recommends nonce- or hash-based policies instead of unsafe inline execution.

Similarly, avoid 'unsafe-eval' unless a library truly requires it. MDN points out that strict CSP is intended to disable risky APIs such as eval(), and allowing it reopens attack surface that CSP is meant to close. If a frontend library needs it, consider replacing that library or isolating the requirement carefully.

Step 10: Add only the external domains you really need

Most real Django projects use at least a few third-party services: Google Fonts, analytics, payment widgets, CDNs, reCAPTCHA, maps, or embedded videos. Add them per directive, not globally. A policy like this is better than opening everything broadly:

from django.utils.csp import CSP

SECURE_CSP_REPORT_ONLY = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE, "https://www.google.com", "https://www.gstatic.com"],
    "style-src": [CSP.SELF, "https://fonts.googleapis.com"],
    "font-src": [CSP.SELF, "https://fonts.gstatic.com"],
    "img-src": [CSP.SELF, "data:"],
    "frame-src": ["https://www.google.com"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.NONE],
    "frame-ancestors": [CSP.NONE],
    "report-uri": "/csp-report/",
}

MDN warns that large allowlist-based CSPs can become hard to maintain and less effective, which is one reason strict nonce- or hash-based policies are preferred. Still, for fonts, frames, and certain integrations, scoped source allowlists are normal and necessary.

The rule is simple: if a resource type does not need a remote domain, do not add one. For example, do not place analytics domains in img-src or payment domains in font-src unless the browser actually needs them there. Fine-grained directives keep the policy understandable and safer.

Step 11: Add a stricter production policy

Once report-only logs are clean and your templates are refactored, promote the policy to enforcement. In Django 6+, move your final version from SECURE_CSP_REPORT_ONLY to SECURE_CSP:

from django.utils.csp import CSP

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "style-src": [CSP.SELF, CSP.NONCE],
    "img-src": [CSP.SELF, "data:"],
    "font-src": [CSP.SELF],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.NONE],
    "frame-ancestors": [CSP.NONE],
}

Django documents SECURE_CSP as the enforcing configuration and SECURE_CSP_REPORT_ONLY as the monitoring configuration. The browser will now block non-compliant resources instead of merely reporting them.

A strong production CSP is intentionally smaller than a permissive one. The goal is not to enumerate every possible convenience. The goal is to declare the smallest trusted surface your Django app actually needs. MDN’s guidance is clear that strict CSPs are the best CSP-based mitigation against XSS.

Step 12: Consider strict-dynamic only if you understand its effect

MDN explains that 'strict-dynamic' extends trust from a nonce- or hash-authorized script to scripts it loads dynamically. It is useful in some JavaScript-heavy applications, especially when trusted scripts load additional scripts. But MDN also notes that when 'strict-dynamic' is present, host source expressions such as 'self' and other allowlist values are ignored for that directive in supporting browsers.

That makes strict-dynamic powerful, but also easy to misunderstand. For many Django projects with modest JavaScript, you do not need it on day one. Start with a nonce-based policy and add strict-dynamic only after you clearly understand your script loading model.

Step 13: Test Django admin separately

The Django admin often behaves differently from your public templates because it may include its own JavaScript, styles, widgets, and plugins. If your staff area uses rich text editors, autocomplete widgets, or custom admin enhancements, they may trigger CSP violations that your public site never sees. Django’s CSP documentation explains the mechanics, but the operational lesson is yours: treat admin pages as a separate CSP test surface.

A common rollout pattern is to get the public site clean first, then test /admin/ thoroughly, then adjust only the directives truly needed. Avoid the mistake of globally weakening the whole site just because the admin needs a temporary exception. Keep the policy disciplined and purposeful. That approach fits MDN’s recommendation to iteratively identify failures and work around them safely.

Step 14: Keep CSP compatible with your frontend architecture

If your Django project uses plain Django templates and self-hosted static files, CSP is usually straightforward. If you combine Django with React, Vue, Vite, HTMX, Alpine, or third-party widgets, CSP gets more sensitive. Django provides the mechanics, but you still need to design your frontend so it avoids unnecessary inline code and dangerous script patterns. MDN emphasizes that nonce- and hash-based strict CSPs are easier to reason about than giant host allowlists.

For example, if your frontend injects configuration into the page, use a nonce-bearing inline script only for that small configuration block, then load the main bundle from static files. If your JS library relies on eval() or inline event handlers, consider replacing it. CSP should shape architecture choices, not just be bolted on after the fact.

Step 15: A practical final example for a normal Django site

Here is a realistic example for a classic Django project with local static assets, Google Fonts, and a little nonce-based inline configuration:

from django.utils.csp import CSP

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
    "style-src": [CSP.SELF, CSP.NONCE, "https://fonts.googleapis.com"],
    "font-src": [CSP.SELF, "https://fonts.gstatic.com"],
    "img-src": [CSP.SELF, "data:"],
    "connect-src": [CSP.SELF],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.NONE],
    "frame-ancestors": [CSP.NONE],
}

And the matching template usage:

{% load static %}
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>My Django Site</title>
  <link rel="stylesheet" href="{% static 'css/site.css' %}">
  <script nonce="{{ csp_nonce }}">
    window.APP_DEBUG = false;
  </script>
</head>
<body>
  <script src="{% static 'js/site.js' %}"></script>
</body>
</html>

This combination aligns with Django’s documented nonce flow and with MDN’s broader strict-CSP recommendations: keep scripts controlled, avoid unsafe inline execution, disable dangerous legacy features, and permit only the minimal external sources you really use.

Step 16: Common mistakes to avoid

The first mistake is copying an old tutorial that uses pre-4.0 django-csp settings. The project’s current documentation explicitly says version 4.0 introduced a breaking configuration change, so older examples can be misleading.

The second mistake is enabling CSP and immediately adding 'unsafe-inline', 'unsafe-eval', and broad remote domains everywhere just to make errors disappear. That can leave you with a header that looks impressive but provides little real protection. MDN’s guidance is the opposite: move toward a strict policy based on nonces or hashes, not a giant permissive allowlist.

The third mistake is treating report-only mode as optional. It is not. Both the Django docs and MDN make clear that report-only is the safe rollout path because it lets you understand breakage before users feel it.

Step 17: Final rollout checklist

Before you consider CSP complete in your Django project, confirm all of the following:

  1. CSP middleware is enabled.
  2. You started with report-only mode.
  3. Violations were collected and reviewed.
  4. Inline event handlers were removed.
  5. Inline scripts were moved to static files or protected with nonces.
  6. object-src 'none' and base-uri 'none' are present.
  7. External domains were added only where needed.
  8. Admin pages were tested.
  9. Production switched to enforced CSP.
  10. The policy remains documented and maintained whenever frontend dependencies change.

That checklist reflects the combined guidance from Django’s built-in CSP docs, django-csp’s current configuration model, and MDN’s strict CSP recommendations.

Conclusion

Adding CSP to a Django project is not a single setting; it is a controlled migration toward safer rendering and safer script execution. The cleanest path today is to use Django 6.0’s built-in CSP support when available, because Django now natively supports middleware, policy settings, constants, and template nonces. If your project is older, django-csp 4.0 is still a solid route, but you must follow its new configuration format rather than older examples.

The most important idea is this: do not try to “make CSP pass” by allowing everything. Start in report-only mode, remove unsafe inline patterns, use nonces where needed, enforce the narrowest possible policy, and keep refining it as your Django project evolves. That is the step-by-step path to a CSP that actually improves security instead of only looking good in a header scan.