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'?
answer
- a placeholder in script-src
- per-request, lazily generated
- context processor exposes it
- a template tag for external tags
- system check security.W027
basics
~10 sPut 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 linesfrom 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
Recall the three steps: CSP.NONCE in script-src, the csp context processor, and nonce="{{ csp_nonce }}" on the inline tag.
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.
Diagnose blocked scripts from the failure table: missing processor (W027), missing placeholder, an empty directive dropped, cached pages.
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.