Hello! First, some context:
Django 6.0 introduced built-in CSP support, which provides a solid foundation. However, two open tickets expose a gap in the current implementation:
- #36784: tags generated by the form’s
Mediaclass lack anonceattribute when CSP is used - #36825: hardcoded
<script src="...">tags in admin templates have the same issue (cc @Antoliny0919 who is working on this ticket)
These become relevant as soon as a nonce-based restrictive CSP policy is used (for example script-src: [CSP.NONCE, CSP.STRICT_DYNAMIC]), since any script tag without a matching nonce will be blocked by the browser.
(As a side note, #36549 would also benefit from a proper solution, replacing the current documentation-only workaround with something more robust.)
Ticket #36784 outlines four possible approaches:
- Tag-based:
{% with_nonce form.media %} - Filter with explicit nonce:
{{ form.media|with_nonce:csp_nonce }} - Filter with implicit context:
{{ form.media|with_nonce }} - Automatic injection:
{{ form.media }}auto-applies a nonce when available
I explored option 2 in a local PR. As @codingjoe noted, if csp_nonce is not present in the context, this raises VariableDoesNotExist, which makes the approach awkward in practice (I added a test demonstrating this).
Option 4 was implemented in this PR. While it minimizes changes at the call site, it introduces tight coupling between the template engine and CSP. I do not think this aligns well with Django’s design principles; more on that below.
In my view, and after processing all the past and present information, a better approach may be a variant of option 1: explicit template tags provided by the CSP layer itself. Not by form media, not by the template engine, and not via implicit integration across internals.
Concretely, I propose two tags under {% load csp %}:
-
{% csp_nonce %}: outputsnonce="<value>"or nothing if CSP is not active. Intended for inline tags such asscriptorlink:<script src="{% static 'admin/js/theme.js' %}" {% csp_nonce %}></script> -
{% csp_media form.media %}: renders aMediaobject with the nonce applied to all generated<script>and<link>tags:{% load csp %} ... {% csp_media form.media %}
The automatic/automagic approach is appealing, but I believe it comes at too high a cost:
It conflicts with Django's composability principles
Django favors loosely coupled components with explicit interactions. The form media system (`Media`, `Script`, `MediaAsset`) is a rendering primitive responsible for producing HTML. CSP is a security layer responsible for generating nonces and attaching headers. These concerns are orthogonal.Automatic injection requires one layer to become aware of the other. Regardless of the mechanism (thread-locals, ContextVar, special rendering objects, or response post-processing), this introduces implicit behavior across layers and couples rendering to request-scoped security state.
The behavior of `{{ form.media }}` becomes context-dependent
With automatic injection, `{{ form.media }}` would silently depend on whether CSP middleware is active, whether a nonce has been generated, and whether rendering occurs within a request/response cycle. The same expression would produce different output depending on runtime context, which complicates testing, documentation, and reasoning.In the specific case of the PR’s approach, the template engine intercepts rendering of any NonceRenderable instance via a modified render_value_in_context, and calls .render(nonce=context.get("csp_nonce")) instead of the normal __str__ path. This introduces a hidden protocol between the template engine and the media system and alters rendering behavior for all consumers of Media and MediaAsset without any explicit opt-in.
Alternative approaches have similar drawbacks. Thread-locals or ContextVar tie rendering to the request lifecycle, breaking use cases such as management commands or background tasks. Response post-processing is fragile (for example with streaming or compressed responses), forces eager nonce evaluation, and cannot reliably distinguish HTML script tags from similar patterns in JavaScript content.
It does not generalize across template engines
The special rendering object mechanism is specific to Django's template engine (DTL). Making automatic injection work across all supported backends requires falling back to other problematic mechanisms or have the other engines adapting.It sets a precedent for implicit cross-layer behavior
Allowing a security layer to modify output generated by an unrelated layer establishes a pattern that weakens boundaries. Over time, this makes it harder to reason about where responsibilities lie and increases the likelihood of inconsistent design decisions in similar areas.In summary, an explicit tag-based approach preserves clear boundaries. The CSP layer provides the tools, and developers opt in where needed. The intent is visible at the call site, and behavior remains predictable.
The migration path for third-party packages and the admin is straightforward and IMHO is a reasonable trade-off for a solution that remains explicit, composable, and maintainable.
{% csp_nonce %}for manual<script>and<link>tags (addresses #36825){% csp_media form.media %}for Media objects (addresses #36784)- Both live under
{% load csp %}, with graceful no-op behavior when CSP is not active
Thoughts?