skip to content

In Django 6.1, how do you let a marketing page's inline script run under a nonce-based Content Security Policy without 'unsafe-inline'?

level: middleimportance: should knowfreq 35%

answer

  1. a placeholder in script-src
  2. per-request, lazily generated
  3. context processor exposes it
  4. a template tag for external tags
  5. system check security.W027

basics

~10 s

Put CSP.NONCE in script-src, add django.template.context_processors.csp to TEMPLATES, and render <script nonce="{{ csp_nonce }}">. The middleware swaps the placeholder for the per-request nonce in the header.

solid answer

~40 s

`SECURE_CSP = {"script-src": [CSP.SELF, CSP.NONCE], ...}` tells `ContentSecurityPolicyMiddleware` to insert `'nonce-<value>'`. The middleware attaches a `LazyNonce` to each request; the `django.template.context_processors.csp` context processor exposes it as `csp_nonce`, so the inline tag becomes `<script nonce="{{ csp_nonce }}">`. In 6.1, `{% csp_nonce_attr %}` adds the attribute to external `<script src>` and `<link>` tags and can render `form.media` with it. The nonce is generated only when something reads it; if nothing does, the middleware drops the placeholder from the header. So a missing context processor gives an empty `nonce` attribute, no nonce in the header and a blocked script, which the 6.1 check `security.W027` warns about.

code

python · 22 lines
python
from django.utils.csp import CSP

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.SELF, CSP.NONCE],
}

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

go deeper

for a junior

Recall the three steps: CSP.NONCE in script-src, the csp context processor, and nonce="{{ csp_nonce }}" on the inline tag.

for a middle

Explain the lazy nonce: generated on first read, inserted into the header only if read, and shared by both headers; name the 6.1 csp_nonce_attr tag.

for a senior

Diagnose blocked scripts from the failure table: missing processor (W027), missing placeholder, an empty directive dropped, cached pages.

for a principal

Judge when nonces are worth their constraints (no full-page caching, template discipline) versus moving inline code to static files.

## The goal A marketing landing page has a small inline script, say one that initialises a signup widget. The site policy forbids `'unsafe-inline'`, because allowing every inline script would also allow any script an attacker manages to inject. A **nonce** (a number used once) solves this: the server generates a random value per response, puts it in the header as `'nonce-<value>'`, and marks the trusted inline tags with `nonce="<value>"`. The browser runs only the inline tags whose attribute matches. Django 6.x wires the three pieces together. ## The three pieces of wiring 1. **The placeholder in the policy.** Add `CSP.NONCE` to `script-src` (or `style-src`). It is a Django sentinel string, `<CSP_NONCE_SENTINEL>`, not a real CSP keyword. 2. **The context processor.** Add `django.template.context_processors.csp` to the `context_processors` option of your template backend. It exposes the request's nonce as the template variable `csp_nonce`. 3. **The attribute in templates.** For inline tags write `nonce="{{ csp_nonce }}"`. For external tags in 6.1, write `{% csp_nonce_attr %}` inside the tag, or `{% csp_nonce_attr form.media %}` to render a form's media assets with the nonce applied. ```html {% load static %} <script src="{% static 'js/widget.js' %}" {% csp_nonce_attr %}></script> <script nonce="{{ csp_nonce }}"> SignupWidget.mount("#signup"); </script> ``` ## What happens during a request - `process_request` stores a `LazyNonce` on the request. Nothing is generated yet. - When a template reads `csp_nonce` (through the variable or `{% csp_nonce_attr %}`), the nonce is generated with `secrets.token_urlsafe(16)` and cached for the rest of the request. - `process_response` builds the header. If the nonce was generated, the placeholder becomes `'nonce-<value>'`. If it was not, the placeholder is removed, and a directive left with no values is omitted. - The same nonce is used in both the enforced and report-only headers of one response. Laziness saves work on pages without inline code, but it has a consequence worth stating in an interview: the header only carries a nonce when the rendering path actually touched it. ## Failure modes | Mistake | What the browser gets | Result | |---|---|---| | context processor missing | `nonce=""` in HTML; header without a nonce | inline script blocked | | `CSP.NONCE` missing from the policy | a valid `nonce` attribute; header without a nonce | inline script blocked | | policy is `"script-src": [CSP.NONCE]` and the page never reads the nonce | no `script-src` directive at all | scripts fall back to `default-src` | | page served from a full-page cache | an old nonce | broken or reused, depending on the cache | The first mistake is common enough that Django 6.1 added the system check `security.W027`: it warns when the CSP middleware is enabled, a policy contains `CSP.NONCE`, and no template backend has the `csp` context processor. ## Django 6.1 conveniences - `{% csp_nonce_attr %}` for external `<script>` and `<link>` elements, and for `Media` objects. - The admin and the other built-in templates now add nonce attributes to their `<script>`, `<style>` and `<link>` elements when the context processor is configured, so the admin works under a strict nonce policy. ## Checking it in a test A nonce page can be verified end to end with the test client: request the page twice, read the `Content-Security-Policy` header, and assert that the `'nonce-...'` value in the header appears in the body's `nonce` attribute and differs between the two responses. That one test catches a missing context processor (empty attribute), a policy without `CSP.NONCE` (no nonce in the header), and an accidental page cache (the same value twice). Pair it with the deploy-time run of `manage.py check`, which reports `security.W027` in 6.1. ## Limits of the nonce - A nonce authorises a whole `<script>` element; inline event-handler attributes such as `onclick` are not covered and need refactoring into script code. - A nonce must be unpredictable and unique per response. Anything that freezes a rendered page, such as full-page caching, undermines it. - The nonce protects only if injected markup cannot learn it; that is why autoescaping, not CSP, remains the first defence against injection.

  • Why can the header end up with no nonce even though CSP.NONCE is in SECURE_CSP?
    The nonce is lazy. `build_policy()` inserts `'nonce-<value>'` only if something read the request's `LazyNonce` during the response; otherwise it removes the placeholder and omits the directive if nothing is left. A page that never renders `csp_nonce`, or a missing context processor, therefore produces a header without a nonce.
  • Does the 6.1 security.W027 check catch every nonce misconfiguration?
    No. It fires only when the CSP middleware is enabled, a policy contains `CSP.NONCE` and no template backend lists the `csp` context processor. A template that forgets the attribute, a policy without the placeholder, or a cached page all pass the check; they show up only as blocked scripts or violation reports.

saying these in an interview costs you the question

  • CSP.NONCE is a CSP keyword the browser understands directly.
  • The middleware generates a nonce once per process and reuses it.
  • Adding CSP.NONCE to SECURE_CSP is enough; templates need no change.
  • A nonce also allows inline onclick handlers on the same page.
  • Without the context processor the script still runs because the header has the nonce.