CSP nonce support for form Media classes and admin scripts

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 Media class lack a nonce attribute 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:

  1. Tag-based: {% with_nonce form.media %}
  2. Filter with explicit nonce: {{ form.media|with_nonce:csp_nonce }}
  3. Filter with implicit context: {{ form.media|with_nonce }}
  4. 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 %}:

  1. {% csp_nonce %}: outputs nonce="<value>" or nothing if CSP is not active. Intended for inline tags such as script or link:

    <script src="{% static 'admin/js/theme.js' %}" {% csp_nonce %}></script>
    
  2. {% csp_media form.media %}: renders a Media object 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?

10 Likes

Overall I think this is the right way forward. Using the tag approach also has the benefit of forward-compatibility if we end up wanting to add more arguments (unlike a filter which can have at most one argument).

I think it might be good to have guidance on how to use a nonce with jinja2 - I don’t think it’s possible to load Django template tag libraries without some extra steps (but I haven’t gone and checked, so I might be wrong), so either we’ll need some jinja2 specific code, or some useful docs.

3 Likes

I agree the tag based approach seems best to me. @Lily-Foote I looked long long ago and it wasn’t possible to load a django tag in Jinja2, but if the Django tag just calls a function to do the “work” and wraps that, wrapping it with a Jinja2 tag is pretty trivial if anyone needed it.

1 Like

From the discussion on the PR, and given how {{ csp_nonce }} already works, I was leaning to the filter approach, which is option 2. If custom tags are better (for the reasons you give) then that would be where I’d lean. +1

Thanks for the effort here everyone!

1 Like

Nonce-based CSP seems to be considerd the “only” secure way to use CSP these days (per web.dev’s strict CSP guide). So making its adoption as easy as possible seems like something we should prioritize.

This is my favoured approach for form.media. All it requires is detecting a variable called csp_nonce in the context. It doesn’t feel like tight coupling to me and making it automatic makes it a LOT easier to migrate to a secure setup.

The alternatives require that every {{ form.media }} in your site be modified, which could complicate projects migrating to a nonce-based CSP. Finding and modifying all those templates, which may be in third-party packages, sounds painful and churn for little gain.

I went with the automatic detection in my libraries, for example in django-htmx there’s the htmx_script tag which does:

…which calls:

(Trimmed.)

The “use a csp_nonce variable if it exists” protocol is not tied to Django’s CSP middleware per se: it allows for alternative CSP implementations if the user wishes.


This seems like a great idea to help third-party packages.


I think we’d do better not to think of CSP as a “separate layer”, just like CSRF is not quite a “separate layer”. Instead, it’s a security feature baked into the framework and can be activated with minimum fuss. Maybe one day we can even turn nonce-based CSP on by default in the default project template: it’s not much pain if you have it enabled from day one and most third-party packages put nonces on their script tags.

To that end, I’d vote taht the {% csp_nonce %} tag be loaded by default, citing {% csrf_token %} as precedent.

3 Likes

Hey @adamchainz — good post!

I think the tension is here:

I was leaning that kind of way initially until I hit @codingjoe’s comment on the Trac ticket (emphasis added):

Unrelated to the context, my other concern was that you might not trust a 3rd party to use safe script sources. I may trust the package, but not its supply chain. E.G., Django’s GeoAdmin uses scripts from a CDN, which opens the door to a supply chain attack. The automatic solution would be to unknowingly trust those resources. It does go against the idea of CSP a little, where you want to explicitly review and whitelist browser resources.

So the question is, Do we want to auto-apply CSP nonce once as soon as you’re opting into it, or do we want folks to, yes, update each instance, presumably then making sure that it was in fact reviewed?

My point there was that currently one needs to go through and add the nonce="{{ csp_nonce }}" fragment to each script tag in order to enable it. (That’s manual review.)

I think we could rationally make the argument for either way — I’m not sure I’m wedded to one or the other — but we should be consistent at least.

Hey,

Good thing we’re having a discussion. This isn’t clear-cut for me either. I was just leaning towards the tag, but I also agree with @adamchainz. The most insecure option is not using CSP because it’s too complicated.

I also need to correct my previous stance about whitelabeling from media by default. If a 3rd-party app ships its own templates, it can just add a nonce no matter the solution. The explicit review is only really likely for 3rd-party form fields, where people ship form media without an explicit template. My point isn’t gone, but weakened.

To make this more difficult, we could even decide to only automatically add nonce to static files served by the app and not full external URLs. Code-wise, fairly simple.

Now going back to usability and security by default: My currently favorite approach would be to go with an explicit tag AND add a runtime warning if form media is rendered but not covered by the CSP. It requires a bit more code work for me, but it provides users with a tool to deploy CSP with confidence.

Lastly, I think enabling CSP is only the less secure version of resource integrity headers. And there’s much more to do until we get to a fully audited JS supply chain. So I will end with a shameless ad to form a browser platform WG Resolve #69 -- Add browser platform working group by codingjoe · Pull Request #70 · django/dsf-working-groups · GitHub

Cheers!
Joe

1 Like

Great points all around, thanks everyone for engaging! I appreciate that.

Yesterday (before the three more recent posts), I polished and prepared for review the code I had been using to evaluate the options outlined in the first post. This is the PR (two commits, each addressing one ticket): Fixed #36825, #36784 -- Add CSP support for explicit script and link tags and Media asset classes. by nessita · Pull Request #21010 · django/django · GitHub

I truly believe we should not do automatic nonce injection. And more strongly: even if consensus eventually shifts that way, I think doing it by baking nonce magic into the DTL internals would be the wrong approach (remember there are and will be other template engines). In such case, we’d need to consider making Media objects somehow context-aware, that would be a much better direction.

My concerns with magic auto-injection :magic_wand::

  1. It introduces a backwards-incompatible change to rendered HTML. Any project using {{ form.media }} today would silently receive different output once CSP middleware and the context processor are enabled. This kind of subtle change is difficult to debug and does not align well with Django’s stability expectations.

  2. It weakens CSP guarantees. Applying a nonce to a resource effectively allows it to execute regardless of script-src restrictions (it’s a “carte blanche” thing). That decision should be explicit. As Carlton noted, CSP is most valuable when resources are deliberately reviewed and allowlisted; doing this implicitly undermines that model.

  3. CSP middleware and the context processor are both opt-in. Adopting CSP already requires explicit configuration in MIDDLEWARE and TEMPLATES. Nonce application should follow the same pattern: an additional explicit step that also serves as a review point.

  4. In practice, projects using CSP.NONCE typically also configure a reporting endpoint. That mechanism will surface blocked resources, providing a clear signal to update templates. Relying on that feedback loop is preferable to silently allowing all media.

  5. When assets are self-hosted and CSP.SELF is present in script-src, nonces are unnecessary for those resources. Nonces become most relevant under stricter policies, which further supports keeping their application deliberate and explicit.

My recommendation is to keep {% csp_media %} as the explicit mechanism, document it clearly (the PR includes a draft proposal for docs), and rely on violation reports to guide migration. The {% csp_nonce %} tag also provides coverage for third-party templates that render their own script tags, which IMHO addresses Adam’s concern.

Regarding {% load csp %} and the system warning, I am happy to discuss those separately, as they are reasonable points.

2 Likes

That is one of my three hang-ups with the automatic idea:

  • inconsistency with Django 6.0 <script nonce=...> usage
  • bypassing manual review
  • baking a special case for CSP nonces into the heart of the template engine

On the last point, the original proposal for automatic usage did this in django/template/base.py:

def render_value_in_context(value, context):
    ...
    if isinstance(value, NonceRenderable): ...
    ...
    if isinstance(value, NonceRenderable): ...

I hope I’m not being an unreasonable purist when I offer that we should disfavor a solution that does that.


I think we should assume third-party packages will adjust their templates as necessary, being in the best position to evaluate the security tradeoffs (do you really need FontAwesome for a single icon, or can you simply migrate away from it, etc.). Third-party packages that don’t keep up will see people voting with their feet.

2 Likes

We could add a warning that triggers to alert people to this configuration where they need to explicitly ignore it.

Would it be possible to allow for the automatic, with an alternative approach to allow for greater control? This could be a “why not both” situation.

This feels like an extension of the concern #2. Not saying the reasoning isn’t sound, but could be solved with a “why not both?” solution.

I don’t know if I understand why (intelligent) automatic injection here is a problem. The form that defines the media assets should control this piece, correct?

1 Like

From the point of view of template engines, this feels to me most similar to csrf tokens. In that case we have an explicit tag {% csrf_token %} that needs to be explicitly added to forms. This is backed by an implicit context processor, which is always run when the request is present. I think this is a level of magic that most Django developers are reasonably comfortable with. I’m not (yet) convinced that more magic is appropriate for CSP nonces.

3 Likes

I can live with this solution. It’s reasonable enough. I’d prefer it to be loaded automatically, as per Adam’s suggestion, but not a deal breaker.

One of my major gripes with it was that it’d be awkward that a person would have to manage CSP in both the Media class on the python side, as well as use a CSP-specific call on the template side. However, looking at Fixed #36825, #36784 -- Add CSP support for explicit script and link tags and Media asset classes. by nessita · Pull Request #21010 · django/django · GitHub, it doesn’t look like we’re allowing controlling the nonce per Media asset on the form, so that concern is moot.

2 Likes