diff --git a/django/core/checks/__init__.py b/django/core/checks/__init__.py index 7eb526a41c..296e991ddc 100644 --- a/django/core/checks/__init__.py +++ b/django/core/checks/__init__.py @@ -7,6 +7,7 @@ from .registry import Tags, register, run_checks, tag_exists # Import these to force registration of checks import django.core.checks.async_checks # NOQA isort:skip import django.core.checks.caches # NOQA isort:skip +import django.core.checks.compatibility.django_4_0 # NOQA isort:skip import django.core.checks.database # NOQA isort:skip import django.core.checks.files # NOQA isort:skip import django.core.checks.model_checks # NOQA isort:skip diff --git a/django/core/checks/compatibility/django_4_0.py b/django/core/checks/compatibility/django_4_0.py new file mode 100644 index 0000000000..7788629735 --- /dev/null +++ b/django/core/checks/compatibility/django_4_0.py @@ -0,0 +1,18 @@ +from django.conf import settings + +from .. import Error, Tags, register + + +@register(Tags.compatibility) +def check_csrf_trusted_origins(app_configs, **kwargs): + errors = [] + for origin in settings.CSRF_TRUSTED_ORIGINS: + if '://' not in origin: + errors.append(Error( + 'As of Django 4.0, the values in the CSRF_TRUSTED_ORIGINS ' + 'setting must start with a scheme (usually http:// or ' + 'https://) but found %s. See the release notes for details.' + % origin, + id='4_0.E001', + )) + return errors diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 368b51f316..10d678db41 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -15,6 +15,7 @@ from django.urls import get_callable from django.utils.cache import patch_vary_headers from django.utils.crypto import constant_time_compare, get_random_string from django.utils.deprecation import MiddlewareMixin +from django.utils.functional import cached_property from django.utils.http import is_same_domain from django.utils.log import log_response @@ -136,6 +137,13 @@ class CsrfViewMiddleware(MiddlewareMixin): This middleware should be used in conjunction with the {% csrf_token %} template tag. """ + @cached_property + def csrf_trusted_origins_hosts(self): + return [ + urlparse(origin).netloc.lstrip('*') + for origin in settings.CSRF_TRUSTED_ORIGINS + ] + # The _accept and _reject methods currently only exist for the sake of the # requires_csrf_token decorator. def _accept(self, request): @@ -272,7 +280,7 @@ class CsrfViewMiddleware(MiddlewareMixin): # Create a list of all acceptable HTTP referers, including the # current host if it's permitted by ALLOWED_HOSTS. - good_hosts = list(settings.CSRF_TRUSTED_ORIGINS) + good_hosts = list(self.csrf_trusted_origins_hosts) if good_referer is not None: good_hosts.append(good_referer) diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 6e787ef1a4..0dbd15b5c9 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -123,6 +123,9 @@ upgrading Django. * **2_0.W001**: Your URL pattern ```` has a ``route`` that contains ``(?P<``, begins with a ``^``, or ends with a ``$``. This was likely an oversight when migrating from ``url()`` to :func:`~django.urls.path`. +* **4_0.E001**: As of Django 4.0, the values in the + :setting:`CSRF_TRUSTED_ORIGINS` setting must start with a scheme (usually + ``http://`` or ``https://``) but found ````. Caches ------ diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 18eb941ce8..704bee63a4 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -457,15 +457,24 @@ should be ``'HTTP_X_XSRF_TOKEN'``. Default: ``[]`` (Empty list) -A list of hosts which are trusted origins for unsafe requests (e.g. ``POST``). +A list of trusted origins for unsafe requests (e.g. ``POST``). + For a :meth:`secure ` unsafe request, Django's CSRF protection requires that the request have a ``Referer`` header that matches the origin present in the ``Host`` header. This prevents, for example, a ``POST`` request from ``subdomain.example.com`` from succeeding against ``api.example.com``. If you need cross-origin unsafe requests over -HTTPS, continuing the example, add ``"subdomain.example.com"`` to this list. -The setting also supports subdomains, so you could add ``".example.com"``, for -example, to allow access from all subdomains of ``example.com``. +HTTPS, continuing the example, add ``'https://subdomain.example.com'`` to this +list (and/or ``http://...`` if requests originate from an insecure page). + +The setting also supports subdomains, so you could add +``'https://*.example.com'``, for example, to allow access from all subdomains +of ``example.com``. + +.. versionchanged:: 4.0 + + The values in older versions must only include the hostname (possibly with + a leading dot) and not the scheme or an asterisk. .. setting:: DATABASES diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index e15413b501..e4107fdad1 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -307,6 +307,22 @@ Upstream support for Oracle 12.2 ends in March 2022 and for Oracle 18c it ends in June 2021. Django 3.2 will be supported until April 2024. Django 4.0 officially supports Oracle 19c. +.. _csrf-trusted-origins-changes-4.0: + +``CSRF_TRUSTED_ORIGINS`` changes +-------------------------------- + +Format change +~~~~~~~~~~~~~ + +Values in the :setting:`CSRF_TRUSTED_ORIGINS` setting must include the scheme +(e.g. ``'http://'`` or ``'https://'``) instead of only the hostname. + +Also, values that started with a dot, must now also include an asterisk before +the dot. For example, change ``'.example.com'`` to ``'https://*.example.com'``. + +A system check detects any required changes. + Miscellaneous ------------- diff --git a/tests/check_framework/test_4_0_compatibility.py b/tests/check_framework/test_4_0_compatibility.py new file mode 100644 index 0000000000..9f288f252a --- /dev/null +++ b/tests/check_framework/test_4_0_compatibility.py @@ -0,0 +1,27 @@ +from django.core.checks import Error +from django.core.checks.compatibility.django_4_0 import ( + check_csrf_trusted_origins, +) +from django.test import SimpleTestCase +from django.test.utils import override_settings + + +class CheckCSRFTrustedOrigins(SimpleTestCase): + + @override_settings(CSRF_TRUSTED_ORIGINS=['example.com']) + def test_invalid_url(self): + self.assertEqual(check_csrf_trusted_origins(None), [ + Error( + 'As of Django 4.0, the values in the CSRF_TRUSTED_ORIGINS ' + 'setting must start with a scheme (usually http:// or ' + 'https://) but found example.com. See the release notes for ' + 'details.', + id='4_0.E001', + ) + ]) + + @override_settings( + CSRF_TRUSTED_ORIGINS=['http://example.com', 'https://example.com'], + ) + def test_valid_urls(self): + self.assertEqual(check_csrf_trusted_origins(None), []) diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index b1b37c8601..f733d25b02 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -399,7 +399,7 @@ class CsrfViewMiddlewareTestMixin: resp = mw.process_view(req, post_form_view, (), {}) self.assertIsNone(resp) - @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_TRUSTED_ORIGINS=['dashboard.example.com']) + @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_TRUSTED_ORIGINS=['https://dashboard.example.com']) def test_https_csrf_trusted_origin_allowed(self): """ A POST HTTPS request with a referer added to the CSRF_TRUSTED_ORIGINS @@ -414,7 +414,7 @@ class CsrfViewMiddlewareTestMixin: resp = mw.process_view(req, post_form_view, (), {}) self.assertIsNone(resp) - @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_TRUSTED_ORIGINS=['.example.com']) + @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_TRUSTED_ORIGINS=['https://*.example.com']) def test_https_csrf_wildcard_trusted_origin_allowed(self): """ A POST HTTPS request with a referer that matches a CSRF_TRUSTED_ORIGINS