Browse Source

Refs #16010 -- Required CSRF_TRUSTED_ORIGINS setting to include the scheme.

Tim Graham 4 years ago
parent
commit
dba44a7a7a

+ 1 - 0
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

+ 18 - 0
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

+ 9 - 1
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)
 

+ 3 - 0
docs/ref/checks.txt

@@ -123,6 +123,9 @@ upgrading Django.
 * **2_0.W001**: Your URL pattern ``<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 ``<hostname>``.
 
 Caches
 ------

+ 13 - 4
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 <django.http.HttpRequest.is_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
 

+ 16 - 0
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
 -------------
 

+ 27 - 0
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), [])

+ 2 - 2
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