summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes Maron <johannes@maron.family>2026-05-11 17:35:36 +0200
committerJacob Walls <jacobtylerwalls@gmail.com>2026-05-18 21:38:07 -0700
commit3e4e0db66961a48a080ff3ff91f6c0d954261366 (patch)
treeaf88763cafb19fab54308406a3312b03f8557db9
parentfbe902d4a4b8b9dcb371509b25ba8feff3852e64 (diff)
Fixed #36825 -- Extended admin templates so CSP nonce is included if available.
Error pages, admin, and registration templates were updated to use `{% csp_nonce %}` on their explicit `<script>`, `<link>`, and `<style>` HTML elements. Co-authored-by: Antoliny0919 <antoliny0919@gmail.com> Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
-rw-r--r--django/contrib/admin/static/admin/css/forms.css2
-rw-r--r--django/contrib/admin/templates/admin/auth/user/add_form.html2
-rw-r--r--django/contrib/admin/templates/admin/auth/user/change_password.html5
-rw-r--r--django/contrib/admin/templates/admin/base.html16
-rw-r--r--django/contrib/admin/templates/admin/change_form.html11
-rw-r--r--django/contrib/admin/templates/admin/change_list.html15
-rw-r--r--django/contrib/admin/templates/admin/delete_confirmation.html4
-rw-r--r--django/contrib/admin/templates/admin/delete_selected_confirmation.html4
-rw-r--r--django/contrib/admin/templates/admin/index.html2
-rw-r--r--django/contrib/admin/templates/admin/login.html4
-rw-r--r--django/contrib/admin/templates/admin/popup_response.html3
-rw-r--r--django/contrib/admin/templates/admin/prepopulated_fields_js.html3
-rw-r--r--django/contrib/admin/templates/registration/password_change_form.html6
-rw-r--r--django/contrib/admin/templates/registration/password_reset_confirm.html6
-rw-r--r--django/contrib/admin/templates/registration/password_reset_form.html6
-rw-r--r--django/contrib/admindocs/templates/admin_doc/model_detail.html2
-rw-r--r--django/views/csrf.py3
-rw-r--r--django/views/debug.py2
-rw-r--r--django/views/templates/csrf_403.html2
-rw-r--r--docs/releases/6.1.txt5
-rw-r--r--tests/admin_views/test_csp.py111
-rw-r--r--tests/view_tests/tests/test_csrf.py32
22 files changed, 208 insertions, 38 deletions
diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css
index e28006cfa4..f24feba737 100644
--- a/django/contrib/admin/static/admin/css/forms.css
+++ b/django/contrib/admin/static/admin/css/forms.css
@@ -1,5 +1,3 @@
-@import url("widgets.css");
-
/* FORM ROWS */
.form-row {
diff --git a/django/contrib/admin/templates/admin/auth/user/add_form.html b/django/contrib/admin/templates/admin/auth/user/add_form.html
index f5a17dde7d..96eecd3444 100644
--- a/django/contrib/admin/templates/admin/auth/user/add_form.html
+++ b/django/contrib/admin/templates/admin/auth/user/add_form.html
@@ -8,5 +8,5 @@
{% endblock %}
{% block extrahead %}
{{ block.super }}
- <link rel="stylesheet" href="{% static 'admin/css/unusable_password_field.css' %}">
+ <link rel="stylesheet" href="{% static 'admin/css/unusable_password_field.css' %}" {% csp_nonce_attr %}>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html
index 70b68f6de5..e7f97ae97f 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -5,8 +5,9 @@
{% block title %}{% if form.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
{% block extrastyle %}
{{ block.super }}
- <link rel="stylesheet" href="{% static "admin/css/forms.css" %}">
- <link rel="stylesheet" href="{% static 'admin/css/unusable_password_field.css' %}">
+ <link rel="stylesheet" href="{% static "admin/css/widgets.css" %}" {% csp_nonce_attr %}>
+ <link rel="stylesheet" href="{% static "admin/css/forms.css" %}" {% csp_nonce_attr %}>
+ <link rel="stylesheet" href="{% static 'admin/css/unusable_password_field.css' %}" {% csp_nonce_attr %}>
{% endblock %}
{% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.model_name }} change-form{% endblock %}
{% if not is_popup %}
diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html
index 8ef92802a9..b8330c02f5 100644
--- a/django/contrib/admin/templates/admin/base.html
+++ b/django/contrib/admin/templates/admin/base.html
@@ -4,22 +4,22 @@
<head>
<title>{% block title %}{% endblock %}</title>
<meta name="color-scheme" content="light dark" />
-<link rel="stylesheet" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
+<link rel="stylesheet" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}" {% csp_nonce_attr %}>
{% block dark-mode-vars %}
- <link rel="stylesheet" href="{% static "admin/css/dark_mode.css" %}">
- <script src="{% static "admin/js/theme.js" %}"></script>
+ <link rel="stylesheet" href="{% static "admin/css/dark_mode.css" %}" {% csp_nonce_attr %}>
+ <script src="{% static "admin/js/theme.js" %}" {% csp_nonce_attr %}></script>
{% endblock %}
{% if not is_popup and is_nav_sidebar_enabled %}
- <link rel="stylesheet" href="{% static "admin/css/nav_sidebar.css" %}">
- <script src="{% static 'admin/js/nav_sidebar.js' %}" defer></script>
+ <link rel="stylesheet" href="{% static "admin/css/nav_sidebar.css" %}" {% csp_nonce_attr %}>
+ <script src="{% static 'admin/js/nav_sidebar.js' %}" defer {% csp_nonce_attr %}></script>
{% endif %}
{% block extrastyle %}{% endblock %}
-{% if LANGUAGE_BIDI %}<link rel="stylesheet" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}">{% endif %}
+{% if LANGUAGE_BIDI %}<link rel="stylesheet" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}" {% csp_nonce_attr %}>{% endif %}
{% block extrahead %}{% endblock %}
{% block responsive %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- <link rel="stylesheet" href="{% static "admin/css/responsive.css" %}">
- {% if LANGUAGE_BIDI %}<link rel="stylesheet" href="{% static "admin/css/responsive_rtl.css" %}">{% endif %}
+ <link rel="stylesheet" href="{% static "admin/css/responsive.css" %}" {% csp_nonce_attr %}>
+ {% if LANGUAGE_BIDI %}<link rel="stylesheet" href="{% static "admin/css/responsive_rtl.css" %}" {% csp_nonce_attr %}>{% endif %}
{% endblock %}
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE">{% endblock %}
</head>
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 39c883a727..f7dac55e0d 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -3,11 +3,15 @@
{% block title %}{% if errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
{% block extrahead %}{{ block.super }}
-<script src="{% url 'admin:jsi18n' %}"></script>
-{{ media }}
+<script src="{% url 'admin:jsi18n' %}" {% csp_nonce_attr %}></script>
+{% csp_nonce_attr media %}
{% endblock %}
-{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
+{% block extrastyle %}
+ {{ block.super }}
+ <link rel="stylesheet" href="{% static "admin/css/widgets.css" %}" {% csp_nonce_attr %}>
+ <link rel="stylesheet" href="{% static "admin/css/forms.css" %}" {% csp_nonce_attr %}>
+{% endblock %}
{% block coltype %}colM{% endblock %}
@@ -76,6 +80,7 @@
{% if adminform and add %}
data-model-name="{{ opts.model_name }}"
{% endif %}
+ {% csp_nonce_attr %}
async>
</script>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 4bb1b2f03b..872a962740 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -4,16 +4,17 @@
{% block title %}{% if cl.formset and cl.formset.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
{% block extrastyle %}
{{ block.super }}
- <link rel="stylesheet" href="{% static "admin/css/changelists.css" %}">
+ <link rel="stylesheet" href="{% static "admin/css/changelists.css" %}" {% csp_nonce_attr %}>
{% if cl.formset %}
- <link rel="stylesheet" href="{% static "admin/css/forms.css" %}">
+ <link rel="stylesheet" href="{% static "admin/css/widgets.css" %}" {% csp_nonce_attr %}>
+ <link rel="stylesheet" href="{% static "admin/css/forms.css" %}" {% csp_nonce_attr %}>
{% endif %}
{% if cl.formset or action_form %}
- <script src="{% url 'admin:jsi18n' %}"></script>
+ <script src="{% url 'admin:jsi18n' %}" {% csp_nonce_attr %}></script>
{% endif %}
- {{ media.css }}
+ {% csp_nonce_attr media.css %}
{% if not actions_on_top and not actions_on_bottom %}
- <style>
+ <style {% csp_nonce_attr %}>
#changelist table thead th:first-child {width: inherit}
</style>
{% endif %}
@@ -21,8 +22,8 @@
{% block extrahead %}
{{ block.super }}
-{{ media.js }}
-<script src="{% static 'admin/js/filters.js' %}" defer></script>
+{% csp_nonce_attr media.js %}
+<script src="{% static 'admin/js/filters.js' %}" defer {% csp_nonce_attr %}></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}
diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html
index 1c34a84d95..7797f44eb3 100644
--- a/django/contrib/admin/templates/admin/delete_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_confirmation.html
@@ -3,8 +3,8 @@
{% block extrahead %}
{{ block.super }}
- {{ media }}
- <script src="{% static 'admin/js/cancel.js' %}" async></script>
+ {% csp_nonce_attr media %}
+ <script src="{% static 'admin/js/cancel.js' %}" async {% csp_nonce_attr %}></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %}
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
index 40cfdbcc4c..cf503ec123 100644
--- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
@@ -3,8 +3,8 @@
{% block extrahead %}
{{ block.super }}
- {{ media }}
- <script src="{% static 'admin/js/cancel.js' %}" async></script>
+ {% csp_nonce_attr media %}
+ <script src="{% static 'admin/js/cancel.js' %}" async {% csp_nonce_attr %}></script>
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %}
diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
index 6f39d375eb..f728fa88ff 100644
--- a/django/contrib/admin/templates/admin/index.html
+++ b/django/contrib/admin/templates/admin/index.html
@@ -1,7 +1,7 @@
{% extends "admin/base_site.html" %}
{% load i18n static admin_filters %}
-{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/dashboard.css" %}">{% endblock %}
+{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/dashboard.css" %}" {% csp_nonce_attr %}>{% endblock %}
{% block coltype %}colMS{% endblock %}
diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html
index fa0dcbc01d..26af943968 100644
--- a/django/contrib/admin/templates/admin/login.html
+++ b/django/contrib/admin/templates/admin/login.html
@@ -2,8 +2,8 @@
{% load i18n static %}
{% block title %}{% if form.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
-{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/login.css" %}">
-{{ form.media }}
+{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/login.css" %}" {% csp_nonce_attr %}>
+{% csp_nonce_attr form.media %}
{% endblock %}
{% block bodyclass %}{{ block.super }} login{% endblock %}
diff --git a/django/contrib/admin/templates/admin/popup_response.html b/django/contrib/admin/templates/admin/popup_response.html
index 57a1ae3661..eae9dcb07a 100644
--- a/django/contrib/admin/templates/admin/popup_response.html
+++ b/django/contrib/admin/templates/admin/popup_response.html
@@ -4,7 +4,8 @@
<body>
<script id="django-admin-popup-response-constants"
src="{% static "admin/js/popup_response.js" %}"
- data-popup-response="{{ popup_response_data }}">
+ data-popup-response="{{ popup_response_data }}"
+ {% csp_nonce_attr %}>
</script>
</body>
</html>
diff --git a/django/contrib/admin/templates/admin/prepopulated_fields_js.html b/django/contrib/admin/templates/admin/prepopulated_fields_js.html
index dd6e56100d..7080e23785 100644
--- a/django/contrib/admin/templates/admin/prepopulated_fields_js.html
+++ b/django/contrib/admin/templates/admin/prepopulated_fields_js.html
@@ -1,5 +1,6 @@
{% load static %}
<script id="django-admin-prepopulated-fields-constants"
src="{% static "admin/js/prepopulate_init.js" %}"
- data-prepopulated-fields="{{ prepopulated_fields_json }}">
+ data-prepopulated-fields="{{ prepopulated_fields_json }}"
+ {% csp_nonce_attr %}>
</script>
diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html
index 91c99c7fd1..aa1a21f205 100644
--- a/django/contrib/admin/templates/registration/password_change_form.html
+++ b/django/contrib/admin/templates/registration/password_change_form.html
@@ -2,7 +2,11 @@
{% load i18n static %}
{% block title %}{% if form.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
-{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
+{% block extrastyle %}
+ {{ block.super }}
+ <link rel="stylesheet" href="{% static "admin/css/widgets.css" %}" {% csp_nonce_attr %}>
+ <link rel="stylesheet" href="{% static "admin/css/forms.css" %}" {% csp_nonce_attr %}>
+{% endblock %}
{% block userlinks %}
{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> / {% endif %} {% translate 'Change password' %} /
<form id="logout-form" method="post" action="{% url 'admin:logout' %}">
diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html
index ffe51b59f8..ff7fbb3ecb 100644
--- a/django/contrib/admin/templates/registration/password_reset_confirm.html
+++ b/django/contrib/admin/templates/registration/password_reset_confirm.html
@@ -2,7 +2,11 @@
{% load i18n static %}
{% block title %}{% if form.new_password1.errors or form.new_password2.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
-{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
+{% block extrastyle %}
+ {{ block.super }}
+ <link rel="stylesheet" href="{% static "admin/css/widgets.css" %}" {% csp_nonce_attr %}>
+ <link rel="stylesheet" href="{% static "admin/css/forms.css" %}" {% csp_nonce_attr %}>
+{% endblock %}
{% block breadcrumbs %}
<ol class="breadcrumbs">
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html
index 31c84fdcc7..0ec1c039a9 100644
--- a/django/contrib/admin/templates/registration/password_reset_form.html
+++ b/django/contrib/admin/templates/registration/password_reset_form.html
@@ -2,7 +2,11 @@
{% load i18n static %}
{% block title %}{% if form.email.errors %}{% translate "Error:" %} {% endif %}{{ block.super }}{% endblock %}
-{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}
+{% block extrastyle %}
+ {{ block.super }}
+ <link rel="stylesheet" href="{% static "admin/css/widgets.css" %}" {% csp_nonce_attr %}>
+ <link rel="stylesheet" href="{% static "admin/css/forms.css" %}" {% csp_nonce_attr %}>
+{% endblock %}
{% block breadcrumbs %}
<ol class="breadcrumbs">
<li><a href="{% url 'admin:index' %}">{% translate 'Home' %}</a></li>
diff --git a/django/contrib/admindocs/templates/admin_doc/model_detail.html b/django/contrib/admindocs/templates/admin_doc/model_detail.html
index 6cf05d8f1b..0ae776a341 100644
--- a/django/contrib/admindocs/templates/admin_doc/model_detail.html
+++ b/django/contrib/admindocs/templates/admin_doc/model_detail.html
@@ -3,7 +3,7 @@
{% block extrahead %}
{{ block.super }}
-<style>
+<style {% csp_nonce_attr %}>
.module table { width:100%; }
.module table p { padding: 0; margin: 0; }
</style>
diff --git a/django/views/csrf.py b/django/views/csrf.py
index adc629e843..55fd68c703 100644
--- a/django/views/csrf.py
+++ b/django/views/csrf.py
@@ -24,6 +24,7 @@ def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
Default view used when request fails CSRF protection
"""
from django.middleware.csrf import REASON_NO_CSRF_COOKIE, REASON_NO_REFERER
+ from django.template.context_processors import csp
c = {
"title": _("Forbidden"),
@@ -64,7 +65,7 @@ def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
"DEBUG": settings.DEBUG,
"docs_version": get_docs_version(),
"more": _("More information is available with DEBUG=True."),
- }
+ } | csp(request)
try:
t = loader.get_template(template_name)
body = t.render(request=request)
diff --git a/django/views/debug.py b/django/views/debug.py
index f7e141d1c6..caa3d05cd8 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -673,6 +673,8 @@ def technical_404_response(request, exception):
return HttpResponseNotFound(t.render(c))
+@csp_override({})
+@csp_report_only_override({})
def default_urlconf(request):
"""Create an empty URLconf 404 error response."""
with builtin_template_path("default_urlconf.html").open(encoding="utf-8") as fh:
diff --git a/django/views/templates/csrf_403.html b/django/views/templates/csrf_403.html
index 6a0daad5ee..fc367cad04 100644
--- a/django/views/templates/csrf_403.html
+++ b/django/views/templates/csrf_403.html
@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark" />
<meta name="robots" content="NONE,NOARCHIVE">
<title>403 Forbidden</title>
- <style>
+ <style {% csp_nonce_attr %}>
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 8f7172256e..0415856c95 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -282,6 +282,11 @@ CSP
with ``CSP.NONCE`` in a CSP policy but
``django.template.context_processors.csp`` is not configured.
+* CSP nonce attributes are now added on ``<script>``, ``<style>``, and
+ ``<link>`` elements in admin templates and all built-in templates when the
+ :func:`~django.template.context_processors.csp` context processor is
+ configured. See :ref:`csp-nonce-config` for setup instructions.
+
CSRF
~~~~
diff --git a/tests/admin_views/test_csp.py b/tests/admin_views/test_csp.py
new file mode 100644
index 0000000000..1b99e2c363
--- /dev/null
+++ b/tests/admin_views/test_csp.py
@@ -0,0 +1,111 @@
+from django.contrib.auth.models import User
+from django.test import TestCase, modify_settings, override_settings
+from django.urls import reverse
+
+
+@override_settings(
+ ROOT_URLCONF="admin_views.urls",
+ TEMPLATES=[
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.template.context_processors.csp",
+ ],
+ },
+ }
+ ],
+)
+@modify_settings(
+ MIDDLEWARE={"append": "django.middleware.csp.ContentSecurityPolicyMiddleware"}
+)
+class AdminCspNonceTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.superuser = User.objects.create_superuser(
+ username="super", password="secret", email="super@example.com"
+ )
+
+ def setUp(self):
+ self.client.force_login(self.superuser)
+
+ @override_settings(
+ TEMPLATES=[
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ ],
+ },
+ }
+ ],
+ )
+ def test_no_nonce_without_csp_context_processor(self):
+ response = self.client.get(reverse("admin:index"))
+ self.assertNotContains(response, 'nonce="')
+
+ def test_index_base_scripts_have_nonce(self):
+ response = self.client.get(reverse("admin:index"))
+ content = response.content.decode()
+ self.assertRegex(content, r'<script src="[^"]*theme\.js"[^>]*nonce="[^"]+"')
+ self.assertRegex(
+ content, r'<script src="[^"]*nav_sidebar\.js"[^>]*nonce="[^"]+"'
+ )
+
+ def test_index_base_links_have_nonce(self):
+ response = self.client.get(reverse("admin:index"))
+ content = response.content.decode()
+ self.assertRegex(content, r'<link[^>]+base\.css"[^>]*nonce="[^"]+"')
+ self.assertRegex(content, r'<link[^>]+dashboard\.css"[^>]*nonce="[^"]+"')
+
+ def test_change_form_scripts_have_nonce(self):
+ response = self.client.get(
+ reverse("admin:auth_user_change", args=[self.superuser.pk])
+ )
+ content = response.content.decode()
+ self.assertRegex(
+ content, r'<script[^>]*src="[^"]*change_form\.js"[^>]*nonce="[^"]+"'
+ )
+
+ def test_change_form_links_have_nonce(self):
+ response = self.client.get(
+ reverse("admin:auth_user_change", args=[self.superuser.pk])
+ )
+ self.assertRegex(
+ response.content.decode(), r'<link[^>]+forms\.css"[^>]*nonce="[^"]+"'
+ )
+
+ def test_change_list_scripts_have_nonce(self):
+ response = self.client.get(reverse("admin:auth_user_changelist"))
+ self.assertRegex(
+ response.content.decode(),
+ r'<script src="[^"]*filters\.js"[^>]*nonce="[^"]+"',
+ )
+
+ def test_change_list_links_have_nonce(self):
+ response = self.client.get(reverse("admin:auth_user_changelist"))
+ self.assertRegex(
+ response.content.decode(), r'<link[^>]+changelists\.css"[^>]*nonce="[^"]+"'
+ )
+
+ def test_delete_confirmation_script_has_nonce(self):
+ response = self.client.get(
+ reverse("admin:auth_user_delete", args=[self.superuser.pk])
+ )
+ self.assertRegex(
+ response.content.decode(),
+ r'<script src="[^"]*cancel\.js"[^>]*nonce="[^"]+"',
+ )
+
+ def test_login_link_has_nonce(self):
+ self.client.logout()
+ response = self.client.get(reverse("admin:login"))
+ self.assertRegex(
+ response.content.decode(), r'<link[^>]+login\.css"[^>]*nonce="[^"]+"'
+ )
diff --git a/tests/view_tests/tests/test_csrf.py b/tests/view_tests/tests/test_csrf.py
index 2d530cc586..ddf59559aa 100644
--- a/tests/view_tests/tests/test_csrf.py
+++ b/tests/view_tests/tests/test_csrf.py
@@ -2,6 +2,7 @@ from unittest import mock
from django.template import TemplateDoesNotExist
from django.test import Client, RequestFactory, SimpleTestCase, override_settings
+from django.utils.csp import CSP
from django.utils.translation import override
from django.views.csrf import CSRF_FAILURE_TEMPLATE_NAME, csrf_failure
@@ -114,6 +115,37 @@ class CsrfViewTests(SimpleTestCase):
self.assertContains(response, "Test template for CSRF failure", status_code=403)
self.assertIs(response.wsgi_request, response.context.request)
+ @override_settings(
+ TEMPLATES=[
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "OPTIONS": {
+ "loaders": [
+ (
+ "django.template.loaders.locmem.Loader",
+ {CSRF_FAILURE_TEMPLATE_NAME: ("{% csp_nonce_attr %}")},
+ ),
+ ],
+ "context_processors": [
+ "django.template.context_processors.csp",
+ ],
+ },
+ }
+ ],
+ MIDDLEWARE=[
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.middleware.csp.ContentSecurityPolicyMiddleware",
+ ],
+ SECURE_CSP={
+ "default-src": [CSP.NONCE],
+ },
+ )
+ def test_custom_template_with_csp_nonce(self):
+ """A custom CSRF_FAILURE_TEMPLATE_NAME with a CSP nonce is used."""
+ response = self.client.post("/")
+ self.assertContains(response, "nonce=", status_code=403)
+ self.assertIs(response.wsgi_request, response.context.request)
+
def test_custom_template_does_not_exist(self):
"""An exception is raised if a nonexistent template is supplied."""
factory = RequestFactory()