Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 39 additions & 17 deletions django/forms/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,28 @@ def __hash__(self):
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):
if (
attrs
and self.attributes
and (conflicts := attrs.keys() & self.attributes.keys())
):
conflicts = ", ".join(sorted(conflicts))
raise ValueError(
f"{self.__class__.__qualname__} has conflicting attributes: "
f"{conflicts}"
)
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):
"""
Expand Down Expand Up @@ -142,38 +155,47 @@ def _css(self):
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(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) if attrs else "",
)
)
)
for path in self._css[medium]
Expand Down
3 changes: 2 additions & 1 deletion django/template/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.conf import settings
from django.middleware.csp import get_nonce
from django.middleware.csrf import get_token
from django.utils.csp import CONTEXT_KEY as CSP_CONTEXT_KEY
from django.utils.functional import SimpleLazyObject, lazy


Expand Down Expand Up @@ -94,4 +95,4 @@ def csp(request):
"""
Add the CSP nonce to the context.
"""
return {"csp_nonce": get_nonce(request)}
return {CSP_CONTEXT_KEY: get_nonce(request)}
7 changes: 6 additions & 1 deletion django/template/defaulttags.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from django.conf import settings
from django.http import QueryDict
from django.utils import timezone
from django.utils import csp, timezone
from django.utils.datastructures import DeferredSubDict
from django.utils.html import conditional_escape, escape, format_html
from django.utils.lorem_ipsum import paragraphs, words
Expand Down Expand Up @@ -1685,3 +1685,8 @@ def do_with(parser, token):
nodelist = parser.parse(("endwith",))
parser.delete_first_token()
return WithNode(None, None, nodelist, extra_context=extra_context)


@register.simple_tag(takes_context=True)
def csp_nonce_attr(context, media=None):
return csp.nonce_attr(context, media)
19 changes: 16 additions & 3 deletions django/utils/csp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
from enum import StrEnum

from django.utils.functional import SimpleLazyObject, empty
from django.utils.html import format_html

# Template context key for the CSP nonce.
CONTEXT_KEY = "csp_nonce"


class CSP(StrEnum):
Expand Down Expand Up @@ -63,10 +67,10 @@ class LazyNonce(SimpleLazyObject):

Example Django template usage with context processors enabled:

<script{% if csp_nonce %} nonce="{{ csp_nonce }}"...{% endif %}>
<script {% csp_nonce_attr %}></script>

The `{% if %}` block will only render if the nonce has been evaluated
elsewhere.
``{% csp_nonce_attr %}`` will only render the nonce attribute if the nonce
has been evaluated (i.e. accessed) elsewhere in the request/response cycle.

"""

Expand All @@ -77,6 +81,15 @@ def __bool__(self):
return self._wrapped is not empty


def nonce_attr(context, media=None):
nonce = context.get(CONTEXT_KEY)
if media:
return media.render(attrs={"nonce": nonce} if nonce is not None else None)
if nonce is None:
return ""
return format_html('nonce="{}"', nonce)


def generate_nonce():
return secrets.token_urlsafe(16)

Expand Down
26 changes: 24 additions & 2 deletions docs/howto/csp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ To use nonces in your CSP policy, beside the basic config, you need to:
},
]

3. In your templates, add the ``nonce`` attribute to the relevant inline
``<style>`` or ``<script>`` tags, using the ``csp_nonce`` context variable:
3. In your templates, add the nonce to elements that require it:

For inline ``<style>`` or ``<script>`` tags, use the ``csp_nonce`` context
variable directly:

.. code-block:: html+django

Expand All @@ -86,6 +88,26 @@ To use nonces in your CSP policy, beside the basic config, you need to:
// This inline JavaScript will be allowed.
</script>

For external ``<script src="...">`` and ``<link rel="stylesheet">``
elements, use the :ttag:`csp_nonce_attr` template tag:

.. code-block:: html+django

<script src="/path/to/script.js" {% csp_nonce_attr %}></script>
<link rel="stylesheet" href="/path/to/style.css" {% csp_nonce_attr %}>

To render a :class:`~django.forms.Media` object's assets with the nonce
applied to each element, pass the object to the :ttag:`csp_nonce_attr` tag:

.. code-block:: html+django

{% csp_nonce_attr form.media %}

.. versionchanged:: 6.1

The :ttag:`csp_nonce_attr` template tag was added, including support for
rendering :class:`~django.forms.Media` objects.

.. admonition:: Caching and Nonce Reuse

The :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware`
Expand Down
8 changes: 4 additions & 4 deletions docs/internals/contributing/writing-code/unit-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -419,14 +419,14 @@ the lines that were changed or added in the PR. It shows:
When reviewing coverage reports on pull requests, keep these limitations in
mind:

* **Database-specific code:** The CI coverage job runs tests using SQLite on
Windows. Code paths specific to other databases (PostgreSQL, MySQL, Oracle)
* **Database-specific code:** The CI coverage job runs tests using PostgreSQL
on Ubuntu. Code paths specific to other databases (SQLite, MySQL, Oracle)
will appear as "not covered" even if database-specific tests exist. This is
expected and acceptable.

* **Platform-specific code:** Similarly, code that only runs on certain
operating systems (Linux, macOS) will appear as not covered when run on
Windows.
operating systems (Windows, macOS) will appear as not covered when run on
Ubuntu.

* **Coverage doesn't equal quality:** A line being "covered" only means it was
executed during tests. It doesn't guarantee the line is well-tested or that
Expand Down
38 changes: 33 additions & 5 deletions docs/ref/csp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -260,16 +260,43 @@ expression into the CSP header.

To use this nonce in templates, the
:func:`~django.template.context_processors.csp` context processor needs to be
enabled. It adds a ``csp_nonce`` variable to the template context, allowing
inline elements to include a matching ``nonce="{{ csp_nonce }}"`` attribute in
inline scripts or styles.
enabled. It adds a ``csp_nonce`` variable to the template context.

.. versionchanged:: 6.1

The :ttag:`csp_nonce_attr` template tag was added, including support for
rendering :class:`~django.forms.Media` objects.

For inline ``<script>`` and ``<style>`` elements, include the nonce directly
using the context variable:

.. code-block:: html+django

<script nonce="{{ csp_nonce }}">
// This inline JavaScript will be allowed.
</script>

For external ``<script src="...">`` and ``<link rel="stylesheet">`` elements,
use the :ttag:`csp_nonce_attr` template tag:

.. code-block:: html+django

<script src="/path/to/script.js" {% csp_nonce_attr %}></script>
<link rel="stylesheet" href="/path/to/style.css" {% csp_nonce_attr %}>

To render a :class:`~django.forms.Media` object's assets with the nonce
applied, pass the object to the :ttag:`csp_nonce_attr` template tag:

.. code-block:: html+django

{% csp_nonce_attr form.media %}

The browser will only execute inline elements that include a ``nonce=<value>``
attribute matching the one specified in the ``Content-Security-Policy`` (or
``Content-Security-Policy-Report-Only``) header. This mechanism provides
fine-grained control over which inline code is allowed to run.

If a template includes ``{{ csp_nonce }}`` but the policy does not include
If a template includes the CSP nonce but the policy does not include
:attr:`~django.utils.csp.CSP.NONCE`, the HTML will include a nonce attribute,
but the header will lack the required source expression. In this case, the
browser will block the inline script or style (or report it for report-only
Expand All @@ -291,7 +318,8 @@ the nonce and weakens security.

To ensure nonce-based policies remain effective:

* Avoid caching full responses that include ``{{ csp_nonce }}``.
* Avoid caching full responses that include ``{{ csp_nonce }}`` or
:ttag:`csp_nonce_attr`.
* If caching is necessary, use a strategy that injects a fresh nonce on each
request, or consider refactoring your application to avoid inline scripts and
styles altogether.
30 changes: 30 additions & 0 deletions docs/ref/templates/builtins.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,36 @@ Sample usage:
This tag is used for CSRF protection, as described in the documentation for
:doc:`Cross Site Request Forgeries </ref/csrf>`.

.. templatetag:: csp_nonce_attr

``csp_nonce_attr``
------------------

.. versionadded:: 6.1

When called without arguments, renders a ``nonce="<value>"`` HTML attribute
when the :func:`~django.template.context_processors.csp` context processor is
configured, and an empty string otherwise. Use it in ``<script>`` or
``<link>`` elements to allow them under a nonce-based Content Security Policy:

.. code-block:: html+django

<script src="/path/to/script.js" {% csp_nonce_attr %}></script>
<link rel="stylesheet" href="/path/to/style.css" {% csp_nonce_attr %}>

When called with a :class:`~django.forms.Media` object as the argument,
renders its assets with the CSP nonce applied to each ``<script>`` and
``<link>`` element:

.. code-block:: html+django

{% csp_nonce_attr form.media %}

In both forms, if the context processor is not configured, assets are rendered
without a nonce attribute.

See :ref:`csp-nonce` for full configuration details.

.. templatetag:: cycle

``cycle``
Expand Down
6 changes: 5 additions & 1 deletion docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,11 @@ Cache
CSP
~~~

* ...
* The new :ttag:`csp_nonce_attr` template tag renders the CSP nonce attribute
on ``<script>`` and ``<link>`` elements, or renders a
:class:`~django.forms.Media` object's assets with the nonce applied, when the
:func:`~django.template.context_processors.csp` context processor is
configured. See :ref:`csp-nonce` for details.

CSRF
~~~~
Expand Down
4 changes: 4 additions & 0 deletions docs/topics/forms/media.txt
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ HTML:
``Media`` objects
=================

.. class:: Media

Represents a collection of media assets required by a widget or form.

When you interrogate the ``media`` attribute of a widget or form, the
value that is returned is a ``forms.Media`` object. As we have already
seen, the string representation of a ``Media`` object is the HTML
Expand Down
Loading
Loading