From 21de00b097187d1e4e479ea877df64bee22b60c9 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 6 May 2026 14:03:45 -0400 Subject: [PATCH 1/2] Refs #36620 -- Mentioned coverage workflow uses PostgreSQL. Before c507aaf9abeff4b93b7f9bdbc55801f2ccfc2d01, this workflow used to run on SQLite. --- docs/internals/contributing/writing-code/unit-tests.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 657027a0334b..d19616e323fc 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -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 From 5686ce41958b21f0ae5bfa290e8d4420a6349c1b Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:53:58 -0300 Subject: [PATCH 2/2] Fixed #36784 -- Added csp_nonce_attr template tag for CSP nonce inclusion. New default tag `{% csp_nonce_attr %}` was added for explicit CSP nonce inclusion into `', 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( - '', - self.absolute_path(path), - medium, + path.render(attrs=attrs) + if isinstance(path, MediaAsset) + else ( + path.__html__() + if hasattr(path, "__html__") + else format_html( + '', + self.absolute_path(path), + medium, + flatatt(attrs) if attrs else "", + ) ) ) for path in self._css[medium] diff --git a/django/template/context_processors.py b/django/template/context_processors.py index 214972de530e..1ff894ecfe96 100644 --- a/django/template/context_processors.py +++ b/django/template/context_processors.py @@ -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 @@ -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)} diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 3348e6426312..4f43b0242f0d 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -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 @@ -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) diff --git a/django/utils/csp.py b/django/utils/csp.py index 08a9d0752e38..08aaed685af7 100644 --- a/django/utils/csp.py +++ b/django/utils/csp.py @@ -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): @@ -63,10 +67,10 @@ class LazyNonce(SimpleLazyObject): Example Django template usage with context processors enabled: - + - 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. """ @@ -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) diff --git a/docs/howto/csp.txt b/docs/howto/csp.txt index 756f815bf255..f37bb2a82ad9 100644 --- a/docs/howto/csp.txt +++ b/docs/howto/csp.txt @@ -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 - ``