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

This commit is contained in:
Tim Graham 2021-01-12 19:55:02 -05:00 committed by Mariusz Felisiak
parent 9bf5e9418f
commit dba44a7a7a
8 changed files with 89 additions and 7 deletions

View File

@ -7,6 +7,7 @@ from .registry import Tags, register, run_checks, tag_exists
# Import these to force registration of checks # Import these to force registration of checks
import django.core.checks.async_checks # NOQA isort:skip import django.core.checks.async_checks # NOQA isort:skip
import django.core.checks.caches # 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.database # NOQA isort:skip
import django.core.checks.files # NOQA isort:skip import django.core.checks.files # NOQA isort:skip
import django.core.checks.model_checks # NOQA isort:skip import django.core.checks.model_checks # NOQA isort:skip

View File

@ -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

View File

@ -15,6 +15,7 @@ from django.urls import get_callable
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
from django.utils.crypto import constant_time_compare, get_random_string from django.utils.crypto import constant_time_compare, get_random_string
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import cached_property
from django.utils.http import is_same_domain from django.utils.http import is_same_domain
from django.utils.log import log_response 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 %} This middleware should be used in conjunction with the {% csrf_token %}
template tag. 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 # The _accept and _reject methods currently only exist for the sake of the
# requires_csrf_token decorator. # requires_csrf_token decorator.
def _accept(self, request): def _accept(self, request):
@ -272,7 +280,7 @@ class CsrfViewMiddleware(MiddlewareMixin):
# Create a list of all acceptable HTTP referers, including the # Create a list of all acceptable HTTP referers, including the
# current host if it's permitted by ALLOWED_HOSTS. # 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: if good_referer is not None:
good_hosts.append(good_referer) good_hosts.append(good_referer)

View File

@ -123,6 +123,9 @@ upgrading Django.
* **2_0.W001**: Your URL pattern ``<pattern>`` has a ``route`` that contains * **2_0.W001**: Your URL pattern ``<pattern>`` has a ``route`` that contains
``(?P<``, begins with a ``^``, or ends with a ``$``. This was likely an ``(?P<``, begins with a ``^``, or ends with a ``$``. This was likely an
oversight when migrating from ``url()`` to :func:`~django.urls.path`. 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 Caches
------ ------

View File

@ -457,15 +457,24 @@ should be ``'HTTP_X_XSRF_TOKEN'``.
Default: ``[]`` (Empty list) 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 For a :meth:`secure <django.http.HttpRequest.is_secure>` unsafe
request, Django's CSRF protection requires that the request have a ``Referer`` request, Django's CSRF protection requires that the request have a ``Referer``
header that matches the origin present in the ``Host`` header. This prevents, header that matches the origin present in the ``Host`` header. This prevents,
for example, a ``POST`` request from ``subdomain.example.com`` from succeeding for example, a ``POST`` request from ``subdomain.example.com`` from succeeding
against ``api.example.com``. If you need cross-origin unsafe requests over against ``api.example.com``. If you need cross-origin unsafe requests over
HTTPS, continuing the example, add ``"subdomain.example.com"`` to this list. HTTPS, continuing the example, add ``'https://subdomain.example.com'`` to this
The setting also supports subdomains, so you could add ``".example.com"``, for list (and/or ``http://...`` if requests originate from an insecure page).
example, to allow access from all subdomains of ``example.com``.
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 .. setting:: DATABASES

View File

@ -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 in June 2021. Django 3.2 will be supported until April 2024. Django 4.0
officially supports Oracle 19c. 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 Miscellaneous
------------- -------------

View File

@ -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), [])

View File

@ -399,7 +399,7 @@ class CsrfViewMiddlewareTestMixin:
resp = mw.process_view(req, post_form_view, (), {}) resp = mw.process_view(req, post_form_view, (), {})
self.assertIsNone(resp) 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): def test_https_csrf_trusted_origin_allowed(self):
""" """
A POST HTTPS request with a referer added to the CSRF_TRUSTED_ORIGINS 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, (), {}) resp = mw.process_view(req, post_form_view, (), {})
self.assertIsNone(resp) 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): def test_https_csrf_wildcard_trusted_origin_allowed(self):
""" """
A POST HTTPS request with a referer that matches a CSRF_TRUSTED_ORIGINS A POST HTTPS request with a referer that matches a CSRF_TRUSTED_ORIGINS