Permalink
Browse files

Enable referer/origin checks in non https requests

Instead of having differential mechanisms of CSRF checks for
http/https, generalise the referer/origin header for both.

This negates the CSRF_COOKIE_DOMAIN setting, i.e. cross-subdomain
requests are not allowed at all.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
  • Loading branch information...
1 parent 46a2f7b commit 28d35a0f9d7f496595daf2955d6149a3a878b93c @crodjer committed Jun 11, 2012
Showing with 53 additions and 61 deletions.
  1. +42 −49 django/middleware/csrf.py
  2. +11 −12 tests/regressiontests/csrf_tests/tests.py
View
@@ -114,26 +114,17 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
# branches that call reject().
return self._accept(request)
+ # Note that host includes the port.
host = request.META.get('HTTP_HOST', '')
origin = request.META.get('HTTP_ORIGIN')
- good_origin = settings.CSRF_COOKIE_DOMAIN or host
+ good_origin = 'http%s://%s/' % ('s' if request.is_secure() else '', host)
# If origin header exists, use it to check for csrf attacks.
# Origin header is being compared to None here as we need to reject
# requests with origin header as '' too, which otherwise is treated
# as null.
if origin is not None:
-
- # If the good origin starts with a dot (.), it means the cookie
- # is supposed to work across all the subdomains, i.e. an
- # endswith test and a count of dots should be fine here.
- # In case case the actual and the good origin are the same, then
- # too it passes.
- if not ((good_origin.startswith('.')
- and good_origin.count('.') is origin.count('.')
- and origin.endswith(good_origin))
- or origin[origin.find('://')+3:] == good_origin):
-
+ if not same_origin(origin, good_origin):
reason = REASON_BAD_ORIGIN % (origin, good_origin)
logger.warning('Forbidden (%s): %s',
reason, request.path,
@@ -142,46 +133,47 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
'request': request,
}
)
+
return self._reject(request, reason)
- if request.is_secure():
- # Suppose user visits http://example.com/
- # An active network attacker (man-in-the-middle, MITM) sends a
- # POST form that targets https://example.com/detonate-bomb/ and
- # submits it via JavaScript.
- #
- # The attacker will need to provide a CSRF cookie and token, but
- # that's no problem for a MITM and the session-independent
- # nonce we're using. So the MITM can circumvent the CSRF
- # protection. This is true for any HTTP connection, but anyone
- # using HTTPS expects better! For this reason, for
- # https://example.com/ we need additional protection that treats
- # http://example.com/ as completely untrusted. Under HTTPS,
- # Barth et al. found that the Referer header is missing for
- # same-domain requests in only about 0.2% of cases or less, so
- # we can use strict Referer checking.
- referer = request.META.get('HTTP_REFERER')
- if referer is None:
- logger.warning('Forbidden (%s): %s',
- REASON_NO_REFERER, request.path,
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, REASON_NO_REFERER)
+ # Suppose user visits http://example.com/
+ # An active network attacker (man-in-the-middle, MITM) sends a
+ # POST form that targets https://example.com/detonate-bomb/ and
+ # submits it via JavaScript.
+ #
+ # The attacker will need to provide a CSRF cookie and token, but
+ # that's no problem for a MITM and the session-independent
+ # nonce we're using. So the MITM can circumvent the CSRF
+ # protection. This is true for any HTTP connection, but anyone
+ # using HTTPS expects better! For this reason, for
+ # https://example.com/ we need additional protection that treats
+ # http://example.com/ as completely untrusted. Under HTTPS,
+ # Barth et al. found that the Referer header is missing for
+ # same-domain requests in only about 0.2% of cases or less, so
+ # we can use strict Referer checking.
+ referer = request.META.get('HTTP_REFERER')
+ if referer is None:
+ logger.warning('Forbidden (%s): %s',
+ REASON_NO_REFERER, request.path,
+ extra={
+ 'status_code': 403,
+ 'request': request,
+ }
+ )
- # Note that request.get_host() includes the port.
- good_referer = 'https://%s/' % host
- if not same_origin(referer, good_referer):
- reason = REASON_BAD_REFERER % (referer, good_referer)
- logger.warning('Forbidden (%s): %s', reason, request.path,
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, reason)
+ return self._reject(request, REASON_NO_REFERER)
+
+ good_referer = good_origin
+
+ if not same_origin(referer, good_referer):
+ reason = REASON_BAD_REFERER % (referer, good_referer)
+ logger.warning('Forbidden (%s): %s', reason, request.path,
+ extra={
+ 'status_code': 403,
+ 'request': request,
+ }
+ )
+ return self._reject(request, reason)
if csrf_token is None:
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
@@ -214,6 +206,7 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
'request': request,
}
)
+
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
@@ -9,6 +9,8 @@
from django.views.decorators.csrf import csrf_exempt, requires_csrf_token, ensure_csrf_cookie
from django.test.utils import override_settings
+settings.DEBUG = True
+
# Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests
def post_form_response():
@@ -52,16 +54,21 @@ class CsrfViewMiddlewareTest(TestCase):
_csrf_id = "1"
def _get_GET_no_csrf_cookie_request(self):
+
return TestingHttpRequest()
def _get_GET_csrf_cookie_request(self):
req = TestingHttpRequest()
req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie
+ req.META['HTTP_HOST'] = 'www.example.com'
+ req.META['HTTP_REFERER'] = 'http://www.example.com'
+
return req
def _get_POST_csrf_cookie_request(self):
req = self._get_GET_csrf_cookie_request()
req.method = "POST"
+
return req
def _get_POST_no_csrf_cookie_request(self):
@@ -96,6 +103,8 @@ def test_process_response_get_token_used(self):
patched.
"""
req = self._get_GET_no_csrf_cookie_request()
+ req.META['HTTP_HOST'] = 'www.example.com'
+ req.META['HTTP_REFERER'] = 'http://www.exmaple.com'
# Put tests for CSRF_COOKIE_* settings here
with self.settings(CSRF_COOKIE_NAME='myname',
@@ -335,7 +344,7 @@ def view(request):
self.assertTrue(resp2.cookies.get(settings.CSRF_COOKIE_NAME, False))
self.assertTrue('Cookie' in resp2.get('Vary',''))
- @override_settings(CSRF_COOKIE_DOMAIN='.example.com')
+ @override_settings(CSRF_COOKIE_DOMAIN='www.example.com')
def test_good_origin_header(self):
"""
Test if a good origin header is accepted for across subdomain settings.
@@ -346,17 +355,6 @@ def test_good_origin_header(self):
req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
self.assertEqual(None, req2)
- @override_settings(CSRF_COOKIE_DOMAIN='www.example.com')
- def test_good_origin_header_2(self):
- """
- Test if a good origin header is accepted for a single subdomain.
- """
- req = self._get_POST_request_with_token()
- req.META['HTTP_HOST'] = 'www.example.com'
- req.META['HTTP_ORIGIN'] = 'http://www.example.com'
- req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
- self.assertEqual(None, req2)
-
@override_settings(CSRF_COOKIE_DOMAIN='example.com')
def test_good_origin_header_3(self):
"""
@@ -365,6 +363,7 @@ def test_good_origin_header_3(self):
req = self._get_POST_request_with_token()
req.META['HTTP_HOST'] = 'example.com'
req.META['HTTP_ORIGIN'] = 'http://example.com'
+ req.META['HTTP_REFERER'] = 'http://example.com'
req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
self.assertEqual(None, req2)

0 comments on commit 28d35a0

Please sign in to comment.