Commits

Luke Plant  committed 1a51f30

[1.2.X] Fixed #15617 - CSRF referer checking too strict

Thanks to adam for the report.

Backport of [15840] from trunk.

  • Participants
  • Parent commits da6f34f
  • Branches releases/1.2.X

Comments (0)

Files changed (5)

File django/middleware/csrf.py

 from django.core.urlresolvers import get_callable
 from django.utils.cache import patch_vary_headers
 from django.utils.hashcompat import md5_constructor
+from django.utils.http import same_origin
 from django.utils.safestring import mark_safe
 
 _POST_FORM_RE = \
                 if referer is None:
                     return self._reject(request, REASON_NO_REFERER)
 
-                # The following check ensures that the referer is HTTPS,
-                # the domains match and the ports match.  This might be too strict.
+                # Note that request.get_host() includes the port
                 good_referer = 'https://%s/' % request.get_host()
-                if not referer.startswith(good_referer):
+                if not same_origin(referer, good_referer):
                     return self._reject(request, REASON_BAD_REFERER %
                                         (referer, good_referer))
 

File django/utils/http.py

 import re
 import sys
 import urllib
+import urlparse
 from email.Utils import formatdate
 
 from django.utils.encoding import smart_str, force_unicode
     """
     return '"%s"' % etag.replace('\\', '\\\\').replace('"', '\\"')
 
+if sys.version_info >= (2, 6):
+    def same_origin(url1, url2):
+        """
+        Checks if two URLs are 'same-origin'
+        """
+        p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
+        return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port)
+else:
+    # Python 2.4, 2.5 compatibility. This actually works for Python 2.6 and
+    # above, but the above definition is much more obviously correct and so is
+    # preferred going forward.
+    def same_origin(url1, url2):
+        """
+        Checks if two URLs are 'same-origin'
+        """
+        p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
+        return p1[0:2] == p2[0:2]

File tests/regressiontests/csrf_tests/tests.py

         req.META['HTTP_REFERER'] = 'https://www.example.com/somepage'
         req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
         self.assertEquals(None, req2)
+
+    def test_https_good_referer_2(self):
+        """
+        Test that a POST HTTPS request with a good referer is accepted
+        where the referer contains no trailing slash
+        """
+        # See ticket #15617
+        req = self._get_POST_request_with_token()
+        req._is_secure = True
+        req.META['HTTP_HOST'] = 'www.example.com'
+        req.META['HTTP_REFERER'] = 'https://www.example.com'
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+        self.assertEqual(None, req2)

File tests/regressiontests/utils/http.py

+import unittest
+
+from django.utils import http
+
+class TestUtilsHttp(unittest.TestCase):
+
+    def test_same_origin_true(self):
+        # Identical
+        self.assertTrue(http.same_origin('http://foo.com/', 'http://foo.com/'))
+        # One with trailing slash - see #15617
+        self.assertTrue(http.same_origin('http://foo.com', 'http://foo.com/'))
+        self.assertTrue(http.same_origin('http://foo.com/', 'http://foo.com'))
+        # With port
+        self.assertTrue(http.same_origin('https://foo.com:8000', 'https://foo.com:8000/'))
+
+    def test_same_origin_false(self):
+        # Different scheme
+        self.assertFalse(http.same_origin('http://foo.com', 'https://foo.com'))
+        # Different host
+        self.assertFalse(http.same_origin('http://foo.com', 'http://goo.com'))
+        # Different host again
+        self.assertFalse(http.same_origin('http://foo.com', 'http://foo.com.evil.com'))
+        # Different port
+        self.assertFalse(http.same_origin('http://foo.com:8000', 'http://foo.com:8001'))

File tests/regressiontests/utils/tests.py

 from module_loading import *
 from termcolors import *
 from html import *
+from http import *
 from checksums import *
 from text import *
 from simplelazyobject import *