Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Csrf Enhancements #95

Closed
wants to merge 11 commits into from

3 participants

Rohan Jain Luke Plant Paul McMillan
Rohan Jain
crodjer commented May 28, 2012

Pull request for GSoC project on Security Enhancements. This doesn't need to be merged anytime soon.

added some commits May 18, 2012
Rohan Jain Add myself to authors
Signed-off-by: Rohan Jain <crodjer@gmail.com>
54e3cf0
Rohan Jain Remove unused imports
Signed-off-by: Rohan Jain <crodjer@gmail.com>
c3b7905
Rohan Jain Initial origin checking implementation
Use origin header to check reject illegitimate requests. Cookie
domain checks need improvement.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
13ba162
Paul McMillan

This would allow bar.com to attack foobar.com, if it came down to the origin header being the deciding factor.

I improved the behaviour of origin checking in a later commit 85f7037. That should take care of these cases.

Luke Plant
Owner

We should have a properly implemented and tested 'same_origin' function, that implements an RFC exactly, not adhoc domain name parsing and dot counting in the flow of another function.

Rohan Jain
added some commits May 29, 2012
Rohan Jain Emulate browser's cookie domains behaviour
For compatibility with cookie domain setting, origin check emulates
the behaviour of browser cookie-domain validator.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
46a2f7b
Rohan Jain 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>
28d35a0
Rohan Jain Setting for cross site permitted domains
`PERMITTED_DOMAINS`, is a setting, which can take a list of domain
patterns in unix glob format.
Administrators will explicitly mention which domains are allowed to
make requests to the site.

This method is safer then using cookie domain which is vulnerable to
various MITM attacks.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
8d8bebe
Rohan Jain Comments for the generalized referer checking
Signed-off-by: Rohan Jain <crodjer@gmail.com>
5df40b4
Rohan Jain Some more tests for permitted domains
Signed-off-by: Rohan Jain <crodjer@gmail.com>
ee52142
Rohan Jain Permitted domains settings to include csrf
`s/PERMITTED_DOMAINS/CSRF_PERMITTED_DOMAINS`, to express what this
setting directly affects.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
cd6d781
Rohan Jain Use referer checking in absence of origin header
Instead of doing checks for both origin and referer header all the
time, do referer checks only in case of origin header's absence.

Given the purpose of the origin header, it can be relied upon at least
to a level equivalent (or even more) than referer header.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
97733ce
Rohan Jain Split out port from host
The host header also gives out port with it, so it should be split out
of it. Since in a url, the string before the last colon (:) is the
host domain, we grab that one as host.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
e49531f
Luke Plant spookylukey commented on the diff October 13, 2012
django/middleware/csrf.py
((35 lines not shown))
  132
+                    reason = REASON_BAD_ORIGIN % (origin)
  133
+                    logger.warning('Forbidden (%s): %s',
  134
+                                   reason, request.path,
  135
+                        extra={
  136
+                            'status_code': 403,
  137
+                            'request': request,
  138
+                        }
  139
+                    )
  140
+
  141
+                    return self._reject(request, reason)
  142
+            else:
  143
+                # Do a strict referer check in case an origin check succeds.
  144
+                # As far as CSRF is concerned, attackers who are in a position
  145
+                # to perform CSRF attack are not in a position to fake referer
  146
+                # headers.
  147
+
2
Luke Plant Owner

This is doing a strict referer check if there is no Origin header. That's going to cause lots of incorrect failures for the case where the browser is configured not to send a Referer header, or where the network strips the header. According to Barth et al. [1], not sending the Referer header is very rare for same-domain HTTPS (which is why we had it like that before), but not that rare for HTTP - probably because the network cannot strip the header for HTTPS, but can for HTTP. This could be anywhere between 0.5% - 7%.

[1] http://seclab.stanford.edu/websec/csrf/csrf.pdf

Rohan Jain
crodjer added a note October 14, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Luke Plant
Owner

Closing for the reasons described by Rohan

Luke Plant spookylukey closed this October 15, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 11 unique commits by 1 author.

May 29, 2012
Rohan Jain Add myself to authors
Signed-off-by: Rohan Jain <crodjer@gmail.com>
54e3cf0
Rohan Jain Remove unused imports
Signed-off-by: Rohan Jain <crodjer@gmail.com>
c3b7905
Rohan Jain Initial origin checking implementation
Use origin header to check reject illegitimate requests. Cookie
domain checks need improvement.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
13ba162
Jun 12, 2012
Rohan Jain Emulate browser's cookie domains behaviour
For compatibility with cookie domain setting, origin check emulates
the behaviour of browser cookie-domain validator.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
46a2f7b
Rohan Jain 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>
28d35a0
Rohan Jain Setting for cross site permitted domains
`PERMITTED_DOMAINS`, is a setting, which can take a list of domain
patterns in unix glob format.
Administrators will explicitly mention which domains are allowed to
make requests to the site.

This method is safer then using cookie domain which is vulnerable to
various MITM attacks.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
8d8bebe
Jul 07, 2012
Rohan Jain Comments for the generalized referer checking
Signed-off-by: Rohan Jain <crodjer@gmail.com>
5df40b4
Jul 08, 2012
Rohan Jain Some more tests for permitted domains
Signed-off-by: Rohan Jain <crodjer@gmail.com>
ee52142
Rohan Jain Permitted domains settings to include csrf
`s/PERMITTED_DOMAINS/CSRF_PERMITTED_DOMAINS`, to express what this
setting directly affects.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
cd6d781
Rohan Jain Use referer checking in absence of origin header
Instead of doing checks for both origin and referer header all the
time, do referer checks only in case of origin header's absence.

Given the purpose of the origin header, it can be relied upon at least
to a level equivalent (or even more) than referer header.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
97733ce
Rohan Jain Split out port from host
The host header also gives out port with it, so it should be split out
of it. Since in a url, the string before the last colon (:) is the
host domain, we grab that one as host.

Signed-off-by: Rohan Jain <crodjer@gmail.com>
e49531f
This page is out of date. Refresh to see the latest.
1  AUTHORS
@@ -566,6 +566,7 @@ answer newbie questions, and generally made Django that much better:
566 566
     Gasper Zejn <zejn@kiberpipa.org>
567 567
     Jarek Zgoda <jarek.zgoda@gmail.com>
568 568
     Cheng Zhang
  569
+    Rohan Jain <crodjer@gmail.com>
569 570
 
570 571
 A big THANK YOU goes to:
571 572
 
69  django/middleware/csrf.py
@@ -5,23 +5,22 @@
5 5
 against request forgeries from other sites.
6 6
 """
7 7
 
8  
-import hashlib
9 8
 import re
10  
-import random
11 9
 
12 10
 from django.conf import settings
13 11
 from django.core.urlresolvers import get_callable
14 12
 from django.utils.cache import patch_vary_headers
15  
-from django.utils.http import same_origin
  13
+from django.utils.http import domain_permitted
16 14
 from django.utils.log import getLogger
17 15
 from django.utils.crypto import constant_time_compare, get_random_string
18 16
 
19 17
 logger = getLogger('django.request')
20 18
 
21 19
 REASON_NO_REFERER = "Referer checking failed - no Referer."
22  
-REASON_BAD_REFERER = "Referer checking failed - %s does not match %s."
  20
+REASON_BAD_REFERER = "Referer checking failed - %s is not permitted."
23 21
 REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
24 22
 REASON_BAD_TOKEN = "CSRF token missing or incorrect."
  23
+REASON_BAD_ORIGIN = "Origin checking failed - %s is not permitted."
25 24
 
26 25
 CSRF_KEY_LENGTH = 32
27 26
 
@@ -106,6 +105,7 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
106 105
 
107 106
         # Assume that anything not defined as 'safe' by RC2616 needs protection
108 107
         if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
  108
+
109 109
             if getattr(request, '_dont_enforce_csrf_checks', False):
110 110
                 # Mechanism to turn off CSRF checks for test suite.
111 111
                 # It comes after the creation of CSRF cookies, so that
@@ -114,22 +114,37 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
114 114
                 # branches that call reject().
115 115
                 return self._accept(request)
116 116
 
117  
-            if request.is_secure():
118  
-                # Suppose user visits http://example.com/
119  
-                # An active network attacker (man-in-the-middle, MITM) sends a
120  
-                # POST form that targets https://example.com/detonate-bomb/ and
121  
-                # submits it via JavaScript.
122  
-                #
123  
-                # The attacker will need to provide a CSRF cookie and token, but
124  
-                # that's no problem for a MITM and the session-independent
125  
-                # nonce we're using. So the MITM can circumvent the CSRF
126  
-                # protection. This is true for any HTTP connection, but anyone
127  
-                # using HTTPS expects better! For this reason, for
128  
-                # https://example.com/ we need additional protection that treats
129  
-                # http://example.com/ as completely untrusted. Under HTTPS,
130  
-                # Barth et al. found that the Referer header is missing for
131  
-                # same-domain requests in only about 0.2% of cases or less, so
132  
-                # we can use strict Referer checking.
  117
+            host = request.META.get('HTTP_HOST', '')
  118
+            # Note that host includes the port, so we split that out.
  119
+            # If the host has port specified (checked with ':') then, grab the
  120
+            # host domain from the string before the last ':'.
  121
+            host = host.split(':')[-2] if ':' in host else host
  122
+
  123
+            origin = request.META.get('HTTP_ORIGIN')
  124
+            permitted_domains = getattr(settings, 'CSRF_PERMITTED_DOMAINS', [host])
  125
+
  126
+            # If origin header exists, use it to check for csrf attacks.
  127
+            # Origin header is being compared to None here because we need to
  128
+            # reject requests with origin header as '' too, which otherwise is
  129
+            # treated as null.
  130
+            if origin is not None:
  131
+                if not domain_permitted(origin, permitted_domains):
  132
+                    reason = REASON_BAD_ORIGIN % (origin)
  133
+                    logger.warning('Forbidden (%s): %s',
  134
+                                   reason, request.path,
  135
+                        extra={
  136
+                            'status_code': 403,
  137
+                            'request': request,
  138
+                        }
  139
+                    )
  140
+
  141
+                    return self._reject(request, reason)
  142
+            else:
  143
+                # Do a strict referer check in case an origin check succeds.
  144
+                # As far as CSRF is concerned, attackers who are in a position
  145
+                # to perform CSRF attack are not in a position to fake referer
  146
+                # headers.
  147
+
133 148
                 referer = request.META.get('HTTP_REFERER')
134 149
                 if referer is None:
135 150
                     logger.warning('Forbidden (%s): %s',
@@ -139,12 +154,13 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
139 154
                             'request': request,
140 155
                         }
141 156
                     )
  157
+
142 158
                     return self._reject(request, REASON_NO_REFERER)
143 159
 
144  
-                # Note that request.get_host() includes the port.
145  
-                good_referer = 'https://%s/' % request.get_host()
146  
-                if not same_origin(referer, good_referer):
147  
-                    reason = REASON_BAD_REFERER % (referer, good_referer)
  160
+                # Make sure that the http referer matches the permitted domains
  161
+                # pattern.
  162
+                if not domain_permitted(referer, permitted_domains):
  163
+                    reason = REASON_BAD_REFERER % (referer)
148 164
                     logger.warning('Forbidden (%s): %s', reason, request.path,
149 165
                         extra={
150 166
                             'status_code': 403,
@@ -153,6 +169,10 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
153 169
                     )
154 170
                     return self._reject(request, reason)
155 171
 
  172
+            # Legacy token checking method.
  173
+            # TODO: Handle this with permitted domains. Cookies won't work
  174
+            # there, invalidating the whole point of permitted domains
  175
+            # functionality.
156 176
             if csrf_token is None:
157 177
                 # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
158 178
                 # and in this way we can avoid all CSRF attacks, including login
@@ -184,6 +204,7 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
184 204
                         'request': request,
185 205
                     }
186 206
                 )
  207
+
187 208
                 return self._reject(request, REASON_BAD_TOKEN)
188 209
 
189 210
         return self._accept(request)
15  django/utils/http.py
@@ -10,6 +10,8 @@
10 10
 from django.utils.encoding import smart_str, force_unicode
11 11
 from django.utils.functional import allow_lazy
12 12
 
  13
+from fnmatch import fnmatch
  14
+
13 15
 ETAG_MATCH = re.compile(r'(?:W/)?"((?:\\.|[^"])*)"')
14 16
 
15 17
 MONTHS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split()
@@ -213,3 +215,16 @@ def same_origin(url1, url2):
213 215
     """
214 216
     p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
215 217
     return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port)
  218
+
  219
+def domain_permitted(url, permitted_domains):
  220
+    """
  221
+    Check if the url submitted is from a permitted domain
  222
+    """
  223
+    domain = urlparse.urlparse(url).hostname
  224
+
  225
+    for permitted_domain in permitted_domains:
  226
+        # This uses the unix glob filename pattern matching, documented here:
  227
+        # http://docs.python.org/library/fnmatch.html
  228
+        if fnmatch(domain, permitted_domain):
  229
+            return True
  230
+    return False
113  tests/regressiontests/csrf_tests/tests.py
@@ -7,6 +7,9 @@
7 7
 from django.template import RequestContext, Template
8 8
 from django.test import TestCase
9 9
 from django.views.decorators.csrf import csrf_exempt, requires_csrf_token, ensure_csrf_cookie
  10
+from django.test.utils import override_settings
  11
+
  12
+settings.DEBUG = True
10 13
 
11 14
 
12 15
 # Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests
@@ -51,16 +54,21 @@ class CsrfViewMiddlewareTest(TestCase):
51 54
     _csrf_id = "1"
52 55
 
53 56
     def _get_GET_no_csrf_cookie_request(self):
  57
+
54 58
         return TestingHttpRequest()
55 59
 
56 60
     def _get_GET_csrf_cookie_request(self):
57 61
         req = TestingHttpRequest()
58 62
         req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie
  63
+        req.META['HTTP_HOST'] = 'www.example.com'
  64
+        req.META['HTTP_REFERER'] = 'http://www.example.com'
  65
+
59 66
         return req
60 67
 
61 68
     def _get_POST_csrf_cookie_request(self):
62 69
         req = self._get_GET_csrf_cookie_request()
63 70
         req.method = "POST"
  71
+
64 72
         return req
65 73
 
66 74
     def _get_POST_no_csrf_cookie_request(self):
@@ -95,6 +103,8 @@ def test_process_response_get_token_used(self):
95 103
         patched.
96 104
         """
97 105
         req = self._get_GET_no_csrf_cookie_request()
  106
+        req.META['HTTP_HOST'] = 'www.example.com'
  107
+        req.META['HTTP_REFERER'] = 'http://www.exmaple.com'
98 108
 
99 109
         # Put tests for CSRF_COOKIE_* settings here
100 110
         with self.settings(CSRF_COOKIE_NAME='myname',
@@ -333,3 +343,106 @@ def view(request):
333 343
         resp2 = CsrfViewMiddleware().process_response(req, resp)
334 344
         self.assertTrue(resp2.cookies.get(settings.CSRF_COOKIE_NAME, False))
335 345
         self.assertTrue('Cookie' in resp2.get('Vary',''))
  346
+
  347
+    @override_settings(CSRF_PERMITTED_DOMAINS=['www.example.com'])
  348
+    def test_good_origin_header(self):
  349
+        """
  350
+        Test if a good origin header is accepted for across subdomain settings.
  351
+        """
  352
+        req = self._get_POST_request_with_token()
  353
+        req.META['HTTP_HOST'] = 'www.example.com'
  354
+        req.META['HTTP_ORIGIN'] = 'http://www.example.com'
  355
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  356
+        self.assertEqual(None, req2)
  357
+
  358
+    @override_settings(CSRF_PERMITTED_DOMAINS=['example.com'])
  359
+    def test_good_origin_header_3(self):
  360
+        """
  361
+        Test if a good origin header is accepted for a no subdomain.
  362
+        """
  363
+        req = self._get_POST_request_with_token()
  364
+        req.META['HTTP_HOST'] = 'example.com'
  365
+        req.META['HTTP_ORIGIN'] = 'http://example.com'
  366
+        req.META['HTTP_REFERER'] = 'http://example.com'
  367
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  368
+        self.assertEqual(None, req2)
  369
+
  370
+    def test_good_origin_header_4(self):
  371
+        """
  372
+        Test if a good origin header is accepted for no cookie setting.
  373
+        """
  374
+        req = self._get_POST_request_with_token()
  375
+        req.META['HTTP_HOST'] = 'www.example.com'
  376
+        req.META['HTTP_ORIGIN'] = 'http://www.example.com'
  377
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  378
+        self.assertEqual(None, req2)
  379
+
  380
+    def test_bad_origin_header(self):
  381
+        """
  382
+        Test if a bad origin header is rejected for different domain.
  383
+        """
  384
+        req = self._get_POST_request_with_token()
  385
+        req.META['HTTP_HOST'] = 'www.example.com'
  386
+        req.META['HTTP_ORIGIN'] = 'http://www.evil.com'
  387
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  388
+        self.assertEqual(403, req2.status_code)
  389
+
  390
+    @override_settings(CSRF_PERMITTED_DOMAINS=['example.com'])
  391
+    def test_bad_origin_header_2(self):
  392
+        """
  393
+        Test if a bad origin header is rejected for subdomains.
  394
+        """
  395
+        req = self._get_POST_request_with_token()
  396
+        req.META['HTTP_HOST'] = 'www.example.com'
  397
+        req.META['HTTP_ORIGIN'] = 'http://www.example.com'
  398
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  399
+        self.assertEqual(403, req2.status_code)
  400
+
  401
+    def test_bad_origin_header_3(self):
  402
+        """
  403
+        Test if a bad origin header is rejected with no cookie setting.
  404
+        """
  405
+        req = self._get_POST_request_with_token()
  406
+        req.META['HTTP_HOST'] = 'www.example.com'
  407
+        req.META['HTTP_ORIGIN'] = 'http://www.evil.com'
  408
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  409
+        self.assertEqual(403, req2.status_code)
  410
+
  411
+    @override_settings(CSRF_PERMITTED_DOMAINS=['crossdomain.com'])
  412
+    def test_permitted_domains_cross(self):
  413
+        '''
  414
+        Test if permitted cross domains requests work
  415
+        '''
  416
+        req = self._get_POST_request_with_token()
  417
+        req.META['HTTP_HOST'] = 'example.com'
  418
+        req.META['HTTP_ORIGIN'] = 'http://crossdomain.com'
  419
+        req.META['HTTP_REFERER'] = 'http://crossdomain.com'
  420
+
  421
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  422
+        self.assertEqual(None, req2)
  423
+
  424
+    @override_settings(CSRF_PERMITTED_DOMAINS=['example.com', '*.crossdomain.com'])
  425
+    def test_permitted_domains_cross_glob(self):
  426
+        '''
  427
+        Test if permitted cross domains specified in glob foramt work
  428
+        '''
  429
+        req = self._get_POST_request_with_token()
  430
+        req.META['HTTP_HOST'] = 'example.com'
  431
+        req.META['HTTP_ORIGIN'] = 'http://test.crossdomain.com'
  432
+        req.META['HTTP_REFERER'] = 'http://test.crossdomain.com'
  433
+
  434
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  435
+        self.assertEqual(None, req2)
  436
+
  437
+    @override_settings(CSRF_PERMITTED_DOMAINS=['example.com', 'valid.crossdomain.com'])
  438
+    def test_permitted_domains_cross_invalid(self):
  439
+        '''
  440
+        Test if permitted cross domains invalid check works
  441
+        '''
  442
+        req = self._get_POST_request_with_token()
  443
+        req.META['HTTP_HOST'] = 'example.com'
  444
+        req.META['HTTP_ORIGIN'] = 'http://invalid.crossdomain.com'
  445
+        req.META['HTTP_REFERER'] = 'http://invalid.crossdomain.com'
  446
+
  447
+        req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
  448
+        self.assertEqual(403, req2.status_code)
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.