#36784: Add CSP support to Django's script object and media objects
--------------------------------+------------------------------------------
     Reporter:  Johannes Maron  |                    Owner:  Johannes Maron
         Type:  New feature     |                   Status:  assigned
    Component:  Forms           |                  Version:  6.0
     Severity:  Normal          |               Resolution:
     Keywords:                  |             Triage Stage:  Accepted
    Has patch:  1               |      Needs documentation:  0
  Needs tests:  0               |  Patch needs improvement:  1
Easy pickings:  0               |                    UI/UX:  0
--------------------------------+------------------------------------------
Comment (by Natalia Bidart):

 I've left a note on the PR, but wanted to expand here on the design
 question. My recommendation is to go with a more explicit approach: I've
 been brainstorming with some LLMs and investigating different paths. The
 one I settled on is having a `render(attrs=None)` method on `Script` and
 `Media` classes, paired with a template filter that passes the nonce
 explicitly. This keeps CSP and form media as independent concerns (which I
 think it's important), the rendering machinery stays generic, and the
 filter is the only place that knows a nonce is involved and connects the
 two worlds.

 My rationale is that both CSP and form media are opt-in features, and I
 think they should be combined explicitly and not behind the scenes, this
 is why I propose a filter since it makes the intent visible at the call
 site. Rob's suggestion in comment:6 points in the right direction, though
 I'd build on it as follows: rather than a boolean flag on `Script`, let's
 add a generic `attrs` dict parameter in `render`, since this is more
 consistent with `Widget.render(attrs=...)` elsewhere in `django.forms`,
 and is more extensible (and nonce-agnostic). Then the filter bridges both
 sides:

 {{{
 {{ form.media|with_nonce:csp_nonce }}
 }}}

 On the filter vs. tag question: yes, a filter cannot access the template
 context directly, so the nonce must be passed explicitly as the filter
 argument. For me, that's actually a (required) feature: it makes the
 machinery explicit and works regardless of what the variable is named in
 the context (think about an alternative implementation of CSP or a
 different nonce generator).

 Rough sketch (names and logic to be polished):
 {{{#!diff
 diff --git a/django/forms/widgets.py b/django/forms/widgets.py
 index 1bcfeba288..db47f0f1a2 100644
 --- a/django/forms/widgets.py
 +++ b/django/forms/widgets.py
 @@ -82,15 +82,18 @@ class MediaAsset:
          return hash(self._path)

      def __str__(self):
 +        return self.render()
 +
 +    def __repr__(self):
 +        return f"{type(self).__qualname__}({self._path!r})"
 +
 +    def render(self, *, attrs=None):
          return format_html(
              self.element_template,
              path=self.path,
 -            attributes=flatatt(self.attributes),
 +            attributes=flatatt({**(attrs or {}), **self.attributes}),
          )

 -    def __repr__(self):
 -        return f"{type(self).__qualname__}({self._path!r})"
 -
      @property
      def path(self):
          """
 @@ -142,38 +145,47 @@ class Media:
      def _js(self):
          return self.merge(*self._js_lists)

 -    def render(self):
 +    def render(self, *, attrs=None):
          return mark_safe(
              "\n".join(
                  chain.from_iterable(
 -                    getattr(self, "render_" + name)() for name in
 MEDIA_TYPES
 +                    getattr(self, "render_" + name)(attrs=attrs) for name
 in MEDIA_TYPES
                  )
              )
          )

 -    def render_js(self):
 +    def render_js(self, *, attrs=None):
          return [
              (
 -                path.__html__()
 -                if hasattr(path, "__html__")
 -                else format_html('<script src="{}"></script>',
 self.absolute_path(path))
 +                path.render(attrs=attrs)
 +                if isinstance(path, MediaAsset)
 +                else (
 +                    path.__html__()
 +                    if hasattr(path, "__html__")
 +                    else
 Script(self.absolute_path(path)).render(attrs=attrs)
 +                )
              )
              for path in self._js
          ]

 -    def render_css(self):
 +    def render_css(self, *, attrs=None):
          # To keep rendering order consistent, we can't just iterate over
          # items(). We need to sort the keys, and iterate over the sorted
 list.
          media = sorted(self._css)
          return chain.from_iterable(
              [
                  (
 -                    path.__html__()
 -                    if hasattr(path, "__html__")
 -                    else format_html(
 -                        '<link href="{}" media="{}" rel="stylesheet">',
 -                        self.absolute_path(path),
 -                        medium,
 +                    path.render(attrs=attrs)
 +                    if isinstance(path, MediaAsset)
 +                    else (
 +                        path.__html__()
 +                        if hasattr(path, "__html__")
 +                        else format_html(
 +                            '<link href="{}" media="{}" {}
 rel="stylesheet">',
 +                            self.absolute_path(path),
 +                            medium,
 +                            flatatt(attrs or {}),
 +                        )
                      )
                  )
                  for path in self._css[medium]
 diff --git a/django/templatetags/media.py b/django/templatetags/media.py
 new file mode 100644
 index 0000000000..c9c84e9042
 --- /dev/null
 +++ b/django/templatetags/media.py
 @@ -0,0 +1,16 @@
 +from django import template
 +
 +register = template.Library()
 +
 +
 [email protected]
 +def with_nonce(media, nonce):
 +    """
 +    Render a Media object with a CSP nonce applied to all script and link
 tags.
 +
 +    Usage::
 +
 +        {% load media %}
 +        {{ form.media|with_nonce:csp_nonce }}
 +    """
 +    return media.render(attrs={"nonce": nonce} if nonce else None)
 }}}
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36784#comment:20>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019ce4d5dca0-53a1913e-3038-441d-9fb1-b79659ff3d5d-000000%40eu-central-1.amazonses.com.

Reply via email to