How to Set Content-Security-Policy in Django Middleware

This guide is part of the FastAPI & Django Security Middleware reference. Django has no built-in support for Content-Security-Policydjango.middleware.security.SecurityMiddleware sets HSTS, X-Content-Type-Options, and Referrer-Policy, but never CSP. You get CSP into a Django response one of two ways: the django-csp package (the right answer for almost everyone, because it generates a fresh per-request nonce and exposes it to templates), or a hand-rolled middleware class that assigns the header itself. This page gives the exact CSP_* settings, both middleware implementations, the ordering rules, and how to roll back without dropping the header in production.

Configuration Syntax & Exact Values

The target header is a single response header whose value is a semicolon-separated list of directives. A nonce-based production baseline:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-rAnd0mB4se64'; style-src 'self' 'nonce-rAnd0mB4se64'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Annotated directive breakdown:

Directive Value Why
default-src 'self' Fallback for every fetch directive not named explicitly.
script-src 'self' 'nonce-…' Only same-origin scripts and inline scripts carrying the matching nonce attribute execute.
style-src 'self' 'nonce-…' Same model for inline <style>/<link> blocks.
img-src 'self' data: Allows same-origin images and inline data: URIs.
object-src 'none' Kills <object>/<embed> legacy plugin vectors.
base-uri 'self' Blocks <base> tag hijacking that would rebase relative script URLs.
frame-ancestors 'none' Clickjacking control; supersedes X-Frame-Options in modern browsers.

With django-csp the literal 'nonce-…' value is injected per request — you never type a nonce into settings. The package reads structured CSP_* settings (a list/tuple per directive) and serializes them into the header above, splicing in the request nonce wherever you include "'nonce-'" in a source list via CSP_INCLUDE_NONCE_IN.

Server-Side Configuration

django-csp install and settings.py

Install the package and add its middleware:

pip install django-csp
# settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "csp.middleware.CSPMiddleware",          # adds the CSP header to every response
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

# Each directive is a tuple of sources. django-csp serializes them into the header.
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'",)
CSP_IMG_SRC = ("'self'", "data:")
CSP_OBJECT_SRC = ("'none'",)
CSP_BASE_URI = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)

# Generate a per-request nonce and splice it into these directives.
CSP_INCLUDE_NONCE_IN = ("script-src", "style-src")

CSP_INCLUDE_NONCE_IN is the key line: for each named directive, CSPMiddleware generates one cryptographically random nonce per request, appends 'nonce-<value>' to that directive’s source list, and exposes the same value as request.csp_nonce. In templates, stamp it onto every inline tag with the `` template tag:

<script nonce="">
  initDashboard();
</script>
<link rel="stylesheet" href="/static/app.css" nonce="">

Because the nonce changes every request, an injected <script> from a stored-XSS payload has no valid nonce and the browser refuses to run it. Do not add 'unsafe-inline' alongside a nonce in the same directive — a browser that understands nonces ignores 'unsafe-inline', but adding it signals intent to allow arbitrary inline script and defeats the point.

Newer django-csp (4.x) moves these into a single CONTENT_SECURITY_POLICY dict with a "DIRECTIVES" key and a sentinel csp.constants.NONCE. The flat CSP_* settings shown here remain the most widely deployed form; pin your version and match its docs.

A hand-rolled custom middleware class

When adding a dependency is off the table, assign the header yourself. The trade-off: you must generate and thread the nonce manually, and you lose django-csp’s per-view decorators.

# myapp/middleware.py
import secrets


class ContentSecurityPolicyMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Per-request nonce, base64url, ~128 bits of entropy.
        nonce = secrets.token_urlsafe(16)
        request.csp_nonce = nonce  # make it available to templates via a context processor

        response = self.get_response(request)

        policy = (
            "default-src 'self'; "
            f"script-src 'self' 'nonce-{nonce}'; "
            f"style-src 'self' 'nonce-{nonce}'; "
            "img-src 'self' data:; object-src 'none'; "
            "base-uri 'self'; frame-ancestors 'none'"
        )
        # Plain assignment makes this middleware the authoritative source.
        response["Content-Security-Policy"] = policy
        return response

Expose the nonce to templates with a context processor so `` works in any view:

# myapp/context_processors.py
def csp_nonce(request):
    return {"csp_nonce": getattr(request, "csp_nonce", "")}

Register both — the middleware string in MIDDLEWARE and the context processor in TEMPLATES[0]["OPTIONS"]["context_processors"].

MIDDLEWARE ordering

Order matters because Django runs MIDDLEWARE top-to-bottom on the request and bottom-to-top on the response. The CSP middleware must set its header after the view runs and must not be overwritten by anything below it:

Diagnostic & Verification Steps

Inspect the raw header. With nonces enabled, the nonce- token changes on every request:

$ curl -sI https://your-domain.com/ | grep -i content-security-policy
content-security-policy: default-src 'self'; script-src 'self' 'nonce-rAnd0mB4se64'; style-src 'self' 'nonce-rAnd0mB4se64'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Confirm the nonce in the header matches the nonce stamped into the HTML body:

$ curl -s https://your-domain.com/ | grep -o 'nonce="[^"]*"' | head -1
nonce="rAnd0mB4se64"

The two rAnd0mB4se64 values must be identical for that single request. If they differ, the template is rendering a stale or missing nonce and every inline script will be blocked.

Run the deploy check — note it does not validate CSP:

$ python manage.py check --deploy
System check identified no issues (0 silenced).

A clean check --deploy audits only the SECURE_* settings. It has no opinion about CSP because no built-in setting controls it, so a pass is not evidence the header exists. Always confirm CSP with curl -sI. In browser DevTools, blocked resources appear in the Console as Refused to execute inline script because it violates the following Content Security Policy directive.

Edge Cases, Security Implications & Safe Rollback

Static files are served outside Django. In production, Nginx, WhiteNoise at the edge, or a CDN typically serves /static/, so requests for CSS and JS never pass through CSPMiddleware. That is fine — the CSP header only needs to ride on the HTML document response, because the browser applies the document’s policy to the resources it loads. Do not expect to see CSP on a curl -I of a static asset, and do not try to set a nonce on a statically served .css file (the nonce only protects inline blocks in the HTML).

Per-view overrides. django-csp ships decorators for the cases where one view needs a looser or tighter policy:

from csp.decorators import csp_update, csp_exempt

@csp_update(IMG_SRC=["https://cdn.example.com"])  # additive for this view only
def gallery(request): ...

@csp_exempt  # send NO CSP header for this view
def legacy_embed(request): ...

@csp_exempt is a sharp tool — an exempted view has zero CSP protection. Use it only for an isolated endpoint that genuinely cannot run under any policy, never as a quick fix for a broken page.

Roll out with Report-Only first. Switching straight to an enforcing policy on a complex app will break inline scripts you forgot about. Deploy Content-Security-Policy-Report-Only first: the browser reports violations but blocks nothing. With django-csp, set CSP_REPORT_ONLY = True (4.x: "REPORT_ONLY": True in the dict); in a custom class, write the header name Content-Security-Policy-Report-Only instead. Watch the reports until they stop, then flip to the enforcing header.

Safe rollback is non-destructive but drops XSS protection, so do it deliberately:

  1. To loosen, switch the enforcing header to Content-Security-Policy-Report-Only (CSP_REPORT_ONLY = True) rather than deleting it — you keep visibility.
  2. To fully remove, comment out csp.middleware.CSPMiddleware (or your custom class) in MIDDLEWARE.
  3. Run python manage.py check to catch config drift.
  4. Reload workers gracefully: systemctl reload gunicorn or kill -HUP <pid>.
  5. Re-run curl -sI and confirm the other security headers (HSTS, X-Frame-Options) are still present — rolling back CSP must not silently take other headers with it.

Frequently Asked Questions

Does Django have a built-in CSP setting like SECURE_HSTS_SECONDS? No. SecurityMiddleware sets HSTS, X-Content-Type-Options, and Referrer-Policy only. There is no SECURE_CSP_* setting. Use the django-csp package or assign the Content-Security-Policy header in a custom middleware class.

How do I get a per-request nonce into my templates? With django-csp, list the directive in CSP_INCLUDE_NONCE_IN and use the `` template tag on each inline <script>/<style>. In a custom middleware, generate the nonce with secrets.token_urlsafe(16), attach it to request, and expose it through a context processor.

Why is every inline script blocked after I enabled CSP? Either the inline tags are missing the nonce attribute, or the nonce in the HTML does not match the one in the header. Run the two curl checks above and confirm both nonce values are identical for the same request. Do not add 'unsafe-inline' to “fix” it — that disables the nonce protection entirely.

Where does the CSP middleware go in the MIDDLEWARE list? Just under django.middleware.security.SecurityMiddleware, high in the list. Django runs the response phase bottom-to-top, so a high position means nothing below it can overwrite the header. There is no conflict with SecurityMiddleware because it never sets CSP.

Conclusion

Reach for django-csp first — it solves the per-request nonce problem that a hand-rolled class makes tedious and error-prone. Roll out incrementally: in staging, ship Content-Security-Policy-Report-Only (CSP_REPORT_ONLY = True) and watch violation reports until they stop; then enable the enforcing header in staging and confirm with curl -sI that the nonce in the header matches the nonce in the HTML; only then promote the enforcing policy to production, tightening directives one at a time rather than starting permissive.

How django-csp injects a nonce and the CSP header into a Django response A request enters the Django middleware stack, CSPMiddleware generates a per-request nonce exposed as request.csp_nonce, the view renders the template with that nonce, and the same middleware writes the Content-Security-Policy header onto the outgoing response. Browser request MIDDLEWARE stack CSPMiddleware makes request.csp_nonce View renders template nonce="..." on scripts CSPMiddleware writes header on response Response CSP: ... 'nonce-...' + matching HTML
django-csp generates one nonce per request, exposes it to the template as request.csp_nonce, and writes the matching Content-Security-Policy header onto the response.