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
2 changes: 0 additions & 2 deletions django/contrib/admin/static/admin/css/forms.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@import url("widgets.css");

/* FORM ROWS */

.form-row {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
16 changes: 8 additions & 8 deletions django/contrib/admin/templates/admin/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
11 changes: 8 additions & 3 deletions django/contrib/admin/templates/admin/change_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down Expand Up @@ -76,6 +80,7 @@
{% if adminform and add %}
data-model-name="{{ opts.model_name }}"
{% endif %}
{% csp_nonce_attr %}
async>
</script>
{% endblock %}
Expand Down
15 changes: 8 additions & 7 deletions django/contrib/admin/templates/admin/change_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@
{% 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 %}
{% endblock %}

{% 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 %}
Expand Down
4 changes: 2 additions & 2 deletions django/contrib/admin/templates/admin/delete_confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/templates/admin/index.html
Original file line number Diff line number Diff line change
@@ -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 %}

Expand Down
4 changes: 2 additions & 2 deletions django/contrib/admin/templates/admin/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
3 changes: 2 additions & 1 deletion django/contrib/admin/templates/admin/popup_response.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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' %}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
3 changes: 2 additions & 1 deletion django/views/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions django/views/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion django/views/templates/csrf_403.html
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
5 changes: 5 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~

Expand Down
111 changes: 111 additions & 0 deletions tests/admin_views/test_csp.py
Original file line number Diff line number Diff line change
@@ -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="[^"]+"'
)
Loading
Loading