CSP Nonces in Django with django-csp

This guide is part of the FastAPI & Django Security Middleware reference. A nonce-based Content-Security-Policy lets you keep inline <script> blocks without ever writing 'unsafe-inline': the server mints one unguessable token per response, prints it in the Content-Security-Policy header, and stamps the same token onto every inline tag it trusts. The browser runs an inline script only when its nonce attribute matches the header. In Django the django-csp package does all of this for you — it generates the token, splices it into the header, and exposes it to templates as request.csp_nonce. This page gives the exact settings for django-csp 4.x (the CONTENT_SECURITY_POLICY dict) and 3.x (CSP_INCLUDE_NONCE_IN), the MIDDLEWARE ordering that makes it work, the template code, and a Report-Only rollout you can reverse in one line.

Configuration Syntax & Exact Values

The nonce lives inside the script-src (and optionally style-src) directive of the response header. A single request produces one header value like this:

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

Every response gets a different nonce- value. You never type the token yourself — django-csp inserts it. Annotated breakdown of the nonce-bearing directive:

Token Meaning Why it matters
'self' Same-origin external scripts (<script src>). Keeps /static/*.js working alongside the nonce.
'nonce-Xk9pQz2mLbR7' This exact inline block is trusted. Base64 token, min 128 bits of entropy, regenerated per request. Only inline tags whose nonce attribute equals it execute.
absence of 'unsafe-inline' Inline scripts are blocked by default. A modern browser ignores 'unsafe-inline' whenever a nonce or hash is present — so the nonce is your only inline allowance.

The security property depends on freshness. If the token were static or cached, an attacker could read it once and stamp their injected <script> with it. django-csp derives it from secrets.token_urlsafe() on the request object, so a token leaked in a cached HTML fragment is already invalid for the next request.

Per-request nonce lifecycle in django-csp A request enters, the middleware mints a nonce, the template stamps it, the header carries it, and the browser matches them before executing inline scripts. CSPMiddleware mints request.csp_nonce Template nonce="{{ ... }}" on tag CSP header script-src 'nonce-...' Browser match then execute stamp serialize
One nonce is minted per request, then flows into both the HTML and the header for the browser to reconcile.

Server-Side Configuration

Install django-csp

pip install django-csp

django-csp 4.0+ uses a single nested dict; 3.x used flat CSP_* names. Both are shown — pick the one matching your installed version (pip show django-csp).

settings.py — MIDDLEWARE ordering

CSPMiddleware must run so its process_response sets the header on the finished response. Place it directly after Django’s own SecurityMiddleware. It has to sit before any middleware that might short-circuit and return early (caching), and its nonce must be generated before templates render, which happens deep in the view — so ordering below SecurityMiddleware and above the session/common layers is correct:

# settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "csp.middleware.CSPMiddleware",          # sets Content-Security-Policy on every response
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

settings.py — django-csp 4.x (CONTENT_SECURITY_POLICY dict)

In 4.x you add the sentinel constant NONCE to any directive that should carry the per-request token. There is no separate include-list — the presence of NONCE in the directive is the instruction to splice the nonce in:

# settings.py (django-csp >= 4.0)
from csp.constants import NONCE, SELF, NONE

CONTENT_SECURITY_POLICY = {
    "DIRECTIVES": {
        "default-src": [SELF],
        "script-src": [SELF, NONCE],   # NONCE => append 'nonce-<token>' per request
        "style-src": [SELF, NONCE],
        "img-src": [SELF, "data:"],
        "object-src": [NONE],
        "base-uri": [SELF],
        "frame-ancestors": [NONE],
    },
}

SELF, NONE, and NONCE are constants from csp.constants that serialize to the correctly quoted tokens ('self', 'none', 'nonce-…'). Using the constants avoids the single most common mistake — writing "self" instead of "'self'", which produces a directive that matches a host literally named self.

settings.py — django-csp 3.x (CSP_INCLUDE_NONCE_IN)

If you are pinned to 3.x, directives are flat tuples and the nonce is opt-in per directive through CSP_INCLUDE_NONCE_IN:

# settings.py (django-csp 3.x)
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'",)

# For each named directive, generate a nonce and append 'nonce-<token>'.
CSP_INCLUDE_NONCE_IN = ("script-src", "style-src")

Both APIs expose the identical request.csp_nonce to templates, so your HTML does not change when you migrate.

Templates — stamping the nonce

The middleware attaches a lazy csp_nonce to request. Reference it with the {{ request.csp_nonce }} template tag on every inline tag you want to run. Accessing the attribute is what forces the token to materialize, so a page with no inline tags never spends entropy:

{# base.html #}
<script nonce="{{ request.csp_nonce }}">
  window.__CFG__ = { debug: false };
  initDashboard();
</script>

<style nonce="{{ request.csp_nonce }}">
  .flash { display: none; }
</style>

{# External files do NOT need the nonce — 'self' already covers them #}
<script src="{% static 'app.js' %}"></script>

External <script src> and <link rel="stylesheet"> are allowed by 'self' and must not carry a nonce; only inline blocks need one. For DTL to expose request you need django.template.context_processors.request in your TEMPLATES context_processors (Django’s startproject includes it by default).

Does this tag need a nonce? A decision tree distinguishing inline scripts and styles that require a nonce from external resources covered by 'self'. Tag on the page script or style Inline content? code inside the tag Add nonce="{{...}}" inline <script> / <style> No nonce src covered by 'self' yes no (external)
Nonces belong only on inline blocks; external files ride on the 'self' source.

Report-Only rollout

To watch violations without breaking anything, publish a Report-Only policy alongside (or instead of) the enforcing one. The browser evaluates it, logs violations, but never blocks:

# settings.py (django-csp >= 4.0) — enforce nothing, just observe
CONTENT_SECURITY_POLICY_REPORT_ONLY = {
    "DIRECTIVES": {
        "default-src": [SELF],
        "script-src": [SELF, NONCE],
        "report-uri": ["/csp-report/"],
    },
}
# settings.py (django-csp 3.x)
CSP_REPORT_ONLY = True
CSP_REPORT_URI = "/csp-report/"

You can ship CONTENT_SECURITY_POLICY (enforcing) and CONTENT_SECURITY_POLICY_REPORT_ONLY (a stricter candidate) at the same time — django-csp emits both headers, letting you tighten the candidate policy against real traffic before promoting it.

Diagnostic & Verification Steps

Confirm the header changes on every request and that the token in the header matches the HTML. Two back-to-back requests must yield two different nonces:

curl -sI https://staging.example.com/ | grep -i content-security-policy
curl -sI https://staging.example.com/ | grep -i content-security-policy

Expected — the nonce- value differs between the two calls:

content-security-policy: default-src 'self'; script-src 'self' 'nonce-Xk9pQz2mLbR7'; ...
content-security-policy: default-src 'self'; script-src 'self' 'nonce-8fT2wLmA1cV0'; ...

Verify the header token equals the HTML token in a single response (they MUST match or the script is blocked):

curl -s https://staging.example.com/ -D - -o body.html | grep -i 'content-security-policy'
grep -o 'nonce="[^"]*"' body.html | head -1

The base64 string after nonce- in the header must equal the value inside nonce="…" in body.html. In Chrome DevTools, an inline script with a missing or stale nonce prints:

Refused to execute inline script because it violates the following Content Security Policy
directive: "script-src 'self' 'nonce-…'". Either the 'unsafe-inline' keyword, a hash, or a
nonce is required to enable inline execution.

If you see that on a script you own, the tag is missing nonce="{{ request.csp_nonce }}" or a caching layer served a stale page whose HTML nonce no longer matches the fresh header.

Header nonce versus HTML nonce match A comparison showing that execution happens only when the header nonce equals the HTML attribute nonce. Header: 'nonce-Xk9pQz2mLbR7' HTML: nonce="Xk9pQz2mLbR7" equal -> script executes Header: 'nonce-8fT2wLmA1cV0' HTML: nonce="Xk9pQz2mLbR7" (stale) mismatch -> blocked
A cached page whose HTML nonce predates the fresh header is the classic stale-nonce block.

Edge Cases, Security Implications & Safe Rollback

Caching invalidates nonces. A per-request nonce and a cached HTML page are fundamentally incompatible: if a proxy, @cache_page, or a CDN serves the same HTML to two visitors, the second visitor’s fresh header carries a nonce that does not match the cached HTML, and every inline script breaks. Never cache HTML responses that contain nonces. If you must cache a page, drop nonces for that route and use hashes ('sha256-…') for its fixed inline scripts instead, or serve those scripts as external 'self' files.

A nonce does not tame 'unsafe-inline' event handlers. Inline event attributes like onclick="…" and javascript: URLs cannot carry a nonce and stay blocked. Move that logic into a nonce-bearing <script> that calls addEventListener. Adding 'unsafe-inline' to “fix” it silently disables the nonce protection entirely, because browsers ignore 'unsafe-inline' when a nonce is present only for scripts they otherwise trust — mixing them defeats the point.

Third-party scripts need 'strict-dynamic', not more nonces. A nonced loader that injects further <script> tags (analytics, tag managers) will have its children blocked, since the children lack the nonce. Add STRICT_DYNAMIC to script-src; a nonced or hashed script then propagates trust to scripts it creates, and host-allowlist sources are ignored by conforming browsers.

Rollback. The whole feature reverts to observe-only in one line. Switch the enforcing policy to Report-Only (4.x: move the dict to CONTENT_SECURITY_POLICY_REPORT_ONLY and delete CONTENT_SECURITY_POLICY; 3.x: CSP_REPORT_ONLY = True). Violations keep flowing to your report-uri, but nothing is blocked. To remove CSP entirely, delete the settings and drop csp.middleware.CSPMiddleware from MIDDLEWARE — no header is emitted and Django behaves as before.

Conclusion

Roll this out in stages: enable CONTENT_SECURITY_POLICY_REPORT_ONLY with NONCE on script-src in staging first, confirm two requests yield two nonces and that your own inline scripts do not appear in violation reports, then promote the identical dict to the enforcing CONTENT_SECURITY_POLICY in production. Keep a short-lived Report-Only candidate running alongside the enforced policy whenever you tighten a directive, so you see the impact on real traffic before it can block anything.

Frequently Asked Questions

Does {{ request.csp_nonce }} work if I do not add NONCE/CSP_INCLUDE_NONCE_IN? The template variable exists and renders a token, but that token appears nowhere in the header, so the browser has nothing to match against and the inline script is blocked. The nonce must be in both the directive (via NONCE in 4.x or CSP_INCLUDE_NONCE_IN in 3.x) and the tag. Enable both together.

Why does the nonce break when I add @cache_page to a view? @cache_page stores the rendered HTML, including its baked-in nonce, and replays it for later requests whose fresh header carries a different nonce. The mismatch blocks every inline script on cache hits. Exclude nonced pages from HTML caching, or switch those routes to 'sha256-…' hashes which are stable across responses.

Can I use nonces and hashes in the same policy? Yes. script-src accepts both 'nonce-…' and one or more 'sha256-…' sources simultaneously; a script executes if it matches either. Use nonces for dynamic inline blocks and hashes for fixed inline scripts on cacheable pages.

How long should the nonce be? django-csp derives it from secrets.token_urlsafe(), giving roughly 128 bits of entropy, which exceeds the CSP specification’s recommended minimum. You do not configure the length; leave the generation to the package so it stays cryptographically random per request.

Do external <script src> files need a nonce? No. External scripts are authorized by the 'self' (or explicit host) source in script-src. Adding a nonce to them is harmless but unnecessary; nonces exist to authorize inline blocks that have no URL to match against.