From 063990cabc795bd8989814c6e6e03fc148e23509 Mon Sep 17 00:00:00 2001
From: David Benjamin <davidben@mit.edu>
Date: Sat, 7 May 2011 23:21:07 -0400
Subject: [PATCH] Added support for the Origin header in CSRF middleware

Also added documentation and unit tests for it.
---
 django/middleware/csrf.py                 |   29 +++++++++++--
 docs/ref/contrib/csrf.txt                 |    6 +++
 tests/regressiontests/csrf_tests/tests.py |   63 ++++++++++++++++++++++++++++-
 3 files changed, 93 insertions(+), 5 deletions(-)

diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py
index 2f36f18..b412406 100644
--- a/django/middleware/csrf.py
+++ b/django/middleware/csrf.py
@@ -25,6 +25,7 @@ else:
     randrange = random.randrange
 _MAX_CSRF_KEY = 18446744073709551616L     # 2 << 63
 
+REASON_BAD_ORIGIN = "Origin checking failed - %s does not match %s."
 REASON_NO_REFERER = "Referer checking failed - no Referer."
 REASON_BAD_REFERER = "Referer checking failed - %s does not match %s."
 REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
@@ -115,6 +116,28 @@ class CsrfViewMiddleware(object):
                 # any branches that call reject()
                 return self._accept(request)
 
+            # Note that request.get_host() includes the port
+            good_origin = '%s://%s' % (request.is_secure() and 'https' or 'http',
+                                       request.get_host())
+
+            if 'HTTP_ORIGIN' in request.META:
+                # Cookies are unreliable when there are untrusted sites on sibling
+                # domains. foo.example.com can set a cookie scoped to .example.com
+                # that is then visible to bar.example.com. There is a proposal for a
+                # new Origin header which should be a better CSRF check. When the
+                # browser supports it, verify the match. It is currently implemented
+                # by WebKit-based browsers, and Mozilla has a ticket to add support.
+                origin = request.META['HTTP_ORIGIN']
+                if good_origin != origin:
+                    reason = REASON_BAD_ORIGIN % (origin, good_origin)
+                    logger.warning('Forbidden (%s): %s' % (reason, request.path),
+                        extra={
+                            'status_code': 403,
+                            '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
@@ -141,10 +164,8 @@ class CsrfViewMiddleware(object):
                     )
                     return self._reject(request, REASON_NO_REFERER)
 
-                # 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)
+                if not same_origin(referer, good_origin):
+                    reason = REASON_BAD_REFERER % (referer, good_origin)
                     logger.warning('Forbidden (%s): %s' % (reason, request.path),
                         extra={
                             'status_code': 403,
diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt
index 013125a..dee0c37 100644
--- a/docs/ref/contrib/csrf.txt
+++ b/docs/ref/contrib/csrf.txt
@@ -213,6 +213,12 @@ The CSRF protection is based on the following things:
    done for HTTP requests because the presence of the Referer header is not
    reliable enough under HTTP.)
 
+5. When the browser provides the `Origin header`_, it is checked for the correct
+   origin as in the referer checking above.  This provides protection against
+   cross-subdomain attacks for browsers which implement the header.
+
+.. _Origin header: http://www.ietf.org/id/draft-ietf-websec-origin-00.txt
+
 This ensures that only forms that have originated from your Web site can be used
 to POST data back.
 
diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py
index a98a6a4..751a0cc 100644
--- a/tests/regressiontests/csrf_tests/tests.py
+++ b/tests/regressiontests/csrf_tests/tests.py
@@ -51,10 +51,15 @@ class CsrfViewMiddlewareTest(TestCase):
     _csrf_id = "1"
 
     def _get_GET_no_csrf_cookie_request(self):
-        return TestingHttpRequest()
+        req = TestingHttpRequest()
+        # Include some host so request.get_host() doesn't error.
+        req.META['HTTP_HOST'] = 'www.example.com'
+        return req
 
     def _get_GET_csrf_cookie_request(self):
         req = TestingHttpRequest()
+        # Include some host so request.get_host() doesn't error.
+        req.META['HTTP_HOST'] = 'www.example.com'
         req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie
         return req
 
@@ -249,3 +254,59 @@ class CsrfViewMiddlewareTest(TestCase):
         req.META['HTTP_REFERER'] = 'https://www.example.com'
         req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
         self.assertEqual(None, req2)
+
+    def test_bad_origin(self):
+        """
+        Test that a request with a bad origin is rejected
+        """
+        req = self._get_POST_request_with_token()
+        req.META['HTTP_HOST'] = 'www.example.com'
+        req.META['HTTP_ORIGIN'] = 'https://www.evil.org'
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+        self.assertNotEqual(None, req2)
+        self.assertEqual(403, req2.status_code)
+
+    def test_bad_origin_2(self):
+        """
+        Test that a request with a null origin is rejected
+        """
+        req = self._get_POST_request_with_token()
+        req.META['HTTP_HOST'] = 'www.example.com'
+        req.META['HTTP_ORIGIN'] = 'null'
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+        self.assertNotEqual(None, req2)
+        self.assertEqual(403, req2.status_code)
+
+    def test_bad_origin_3(self):
+        """
+        Test that a request with an origin with wrong protocol is rejected
+        """
+        req = self._get_POST_request_with_token()
+        req._is_secure = True
+        req.META['HTTP_HOST'] = 'www.example.com'
+        req.META['HTTP_ORIGIN'] = 'http://example.com'
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+        self.assertNotEqual(None, req2)
+        self.assertEqual(403, req2.status_code)
+
+    def test_good_origin(self):
+        """
+        Test that a POST HTTP request with a good origin is accepted
+        """
+        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)
+
+    def test_good_origin_2(self):
+        """
+        Test that a POST HTTPS request with a good origin is accepted
+        """
+        req = self._get_POST_request_with_token()
+        req._is_secure = True
+        req.META['HTTP_HOST'] = 'www.example.com'
+        req.META['HTTP_ORIGIN'] = 'https://www.example.com'
+        req.META['HTTP_REFERER'] = 'https://www.example.com/somepage'
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+        self.assertEqual(None, req2)
-- 
1.7.3.3.399.g06ddf

