diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 280bfeb167..6b759d8454 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -558,6 +558,7 @@ CSRF_COOKIE_PATH = '/' CSRF_COOKIE_SECURE = False CSRF_COOKIE_HTTPONLY = False CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN' +CSRF_TRUSTED_ORIGINS = [] ############ # MESSAGES # diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 6d16a92bf5..dee5bb1d93 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -19,7 +19,7 @@ from django.utils.http import same_origin logger = logging.getLogger('django.request') REASON_NO_REFERER = "Referer checking failed - no Referer." -REASON_BAD_REFERER = "Referer checking failed - %s does not match %s." +REASON_BAD_REFERER = "Referer checking failed - %s does not match any trusted origins." REASON_NO_CSRF_COOKIE = "CSRF cookie not set." REASON_BAD_TOKEN = "CSRF token missing or incorrect." @@ -154,10 +154,15 @@ class CsrfViewMiddleware(object): if referer is None: return self._reject(request, REASON_NO_REFERER) + # Here we generate a list of all acceptable HTTP referers, + # including the current host since that has been validated + # upstream. + good_hosts = list(settings.CSRF_TRUSTED_ORIGINS) # Note that request.get_host() includes the port. - good_referer = 'https://%s/' % request.get_host() - if not same_origin(referer, good_referer): - reason = REASON_BAD_REFERER % (referer, good_referer) + good_hosts.append(request.get_host()) + good_referers = ['https://{0}/'.format(host) for host in good_hosts] + if not any(same_origin(referer, host) for host in good_referers): + reason = REASON_BAD_REFERER % referer return self._reject(request, reason) if csrf_token is None: diff --git a/docs/ref/csrf.txt b/docs/ref/csrf.txt index 590990571f..ba24339a78 100644 --- a/docs/ref/csrf.txt +++ b/docs/ref/csrf.txt @@ -257,7 +257,8 @@ The CSRF protection is based on the following things: due to the fact that HTTP 'Set-Cookie' headers are (unfortunately) accepted by clients that are talking to a site under HTTPS. (Referer checking is not done for HTTP requests because the presence of the Referer header is not - reliable enough under HTTP.) + reliable enough under HTTP.) Expanding the accepted referers beyond the + current host can be done with the :setting:`CSRF_TRUSTED_ORIGINS` setting. This ensures that only forms that have originated from your Web site can be used to POST data back. @@ -460,3 +461,4 @@ A number of settings can be used to control Django's CSRF behavior: * :setting:`CSRF_COOKIE_SECURE` * :setting:`CSRF_FAILURE_VIEW` * :setting:`CSRF_HEADER_NAME` +* :setting:`CSRF_TRUSTED_ORIGINS` diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 217f54281d..ed5ac98947 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -428,6 +428,23 @@ any hyphens with underscores, and adding an ``'HTTP_'`` prefix to the name. For example, if your client sends a ``'X-XSRF-TOKEN'`` header, the setting should be ``'HTTP_X_XSRF_TOKEN'``. +.. setting:: CSRF_TRUSTED_ORIGINS + +CSRF_TRUSTED_ORIGINS +-------------------- + +.. versionadded:: 1.9 + +Default: ``[]`` (Empty list) + +A list of hosts which are 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. + .. setting:: DATABASES DATABASES @@ -3374,6 +3391,7 @@ Security * :setting:`CSRF_COOKIE_SECURE` * :setting:`CSRF_FAILURE_VIEW` * :setting:`CSRF_HEADER_NAME` + * :setting:`CSRF_TRUSTED_ORIGINS` * :setting:`SECRET_KEY` * :setting:`X_FRAME_OPTIONS` diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 5c0e3b613c..cf7909057b 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -484,6 +484,9 @@ CSRF * The request header's name used for CSRF authentication can be customized with :setting:`CSRF_HEADER_NAME`. +* The new :setting:`CSRF_TRUSTED_ORIGINS` setting provides a way to allow + cross-origin unsafe requests (e.g. ``POST``) over HTTPS. + Signals ^^^^^^^ diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 691ab2f4ef..1edaa68d49 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -644,6 +644,7 @@ refactoring refactorings refactors referer +referers reflow regex regexes diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index 2d347ccdde..aaa9a6969a 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -352,6 +352,19 @@ class CsrfViewMiddlewareTest(SimpleTestCase): req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) self.assertIsNone(req2) + @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_TRUSTED_ORIGINS=['dashboard.example.com']) + def test_https_csrf_trusted_origin_allowed(self): + """ + A POST HTTPS request with a referer added to the CSRF_TRUSTED_ORIGINS + setting is accepted. + """ + req = self._get_POST_request_with_token() + req._is_secure_override = True + req.META['HTTP_HOST'] = 'www.example.com' + req.META['HTTP_REFERER'] = 'https://dashboard.example.com' + req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) + self.assertIsNone(req2) + def test_ensures_csrf_cookie_no_middleware(self): """ Tests that ensures_csrf_cookie decorator fulfils its promise