Fixed #25334 -- Provided a way to allow cross-origin unsafe requests over HTTPS.

Added the CSRF_TRUSTED_ORIGINS setting which contains a list of other
domains that are included during the CSRF Referer header verification
for secure (HTTPS) requests.
This commit is contained in:
Joshua Kehn 2015-08-31 22:32:03 -04:00 committed by Tim Graham
parent 48c420d992
commit ab26b65b2f
7 changed files with 48 additions and 5 deletions

View File

@ -558,6 +558,7 @@ CSRF_COOKIE_PATH = '/'
CSRF_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False
CSRF_COOKIE_HTTPONLY = False CSRF_COOKIE_HTTPONLY = False
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN' CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_TRUSTED_ORIGINS = []
############ ############
# MESSAGES # # MESSAGES #

View File

@ -19,7 +19,7 @@ from django.utils.http import same_origin
logger = logging.getLogger('django.request') logger = logging.getLogger('django.request')
REASON_NO_REFERER = "Referer checking failed - no Referer." 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_NO_CSRF_COOKIE = "CSRF cookie not set."
REASON_BAD_TOKEN = "CSRF token missing or incorrect." REASON_BAD_TOKEN = "CSRF token missing or incorrect."
@ -154,10 +154,15 @@ class CsrfViewMiddleware(object):
if referer is None: if referer is None:
return self._reject(request, REASON_NO_REFERER) 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. # Note that request.get_host() includes the port.
good_referer = 'https://%s/' % request.get_host() good_hosts.append(request.get_host())
if not same_origin(referer, good_referer): good_referers = ['https://{0}/'.format(host) for host in good_hosts]
reason = REASON_BAD_REFERER % (referer, good_referer) if not any(same_origin(referer, host) for host in good_referers):
reason = REASON_BAD_REFERER % referer
return self._reject(request, reason) return self._reject(request, reason)
if csrf_token is None: if csrf_token is None:

View File

@ -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 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 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 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 This ensures that only forms that have originated from your Web site can be used
to POST data back. 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_COOKIE_SECURE`
* :setting:`CSRF_FAILURE_VIEW` * :setting:`CSRF_FAILURE_VIEW`
* :setting:`CSRF_HEADER_NAME` * :setting:`CSRF_HEADER_NAME`
* :setting:`CSRF_TRUSTED_ORIGINS`

View File

@ -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 For example, if your client sends a ``'X-XSRF-TOKEN'`` header, the setting
should be ``'HTTP_X_XSRF_TOKEN'``. 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 <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.
.. setting:: DATABASES .. setting:: DATABASES
DATABASES DATABASES
@ -3374,6 +3391,7 @@ Security
* :setting:`CSRF_COOKIE_SECURE` * :setting:`CSRF_COOKIE_SECURE`
* :setting:`CSRF_FAILURE_VIEW` * :setting:`CSRF_FAILURE_VIEW`
* :setting:`CSRF_HEADER_NAME` * :setting:`CSRF_HEADER_NAME`
* :setting:`CSRF_TRUSTED_ORIGINS`
* :setting:`SECRET_KEY` * :setting:`SECRET_KEY`
* :setting:`X_FRAME_OPTIONS` * :setting:`X_FRAME_OPTIONS`

View File

@ -484,6 +484,9 @@ CSRF
* The request header's name used for CSRF authentication can be customized * The request header's name used for CSRF authentication can be customized
with :setting:`CSRF_HEADER_NAME`. 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 Signals
^^^^^^^ ^^^^^^^

View File

@ -644,6 +644,7 @@ refactoring
refactorings refactorings
refactors refactors
referer referer
referers
reflow reflow
regex regex
regexes regexes

View File

@ -352,6 +352,19 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
self.assertIsNone(req2) 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): def test_ensures_csrf_cookie_no_middleware(self):
""" """
Tests that ensures_csrf_cookie decorator fulfils its promise Tests that ensures_csrf_cookie decorator fulfils its promise