Code

Ticket #10816: csrf-remove-session-dep-r10369.2.diff

File csrf-remove-session-dep-r10369.2.diff, 19.4 KB (added by Glenn, 5 years ago)

Add a prefix when using SECRET_KEY (see http://code.djangoproject.com/ticket/9977#comment:14)

Line 
1Index: django/contrib/csrf/middleware.py
2===================================================================
3--- django/contrib/csrf/middleware.py   (revision 10369)
4+++ django/contrib/csrf/middleware.py   (working copy)
5@@ -7,6 +7,7 @@
6 
7 import re
8 import itertools
9+import random
10 try:
11     from functools import wraps
12 except ImportError:
13@@ -24,29 +25,69 @@
14 
15 _HTML_TYPES = ('text/html', 'application/xhtml+xml')
16 
17-def _make_token(session_id):
18-    return md5_constructor(settings.SECRET_KEY + session_id).hexdigest()
19+# Use the system (hardware-based) random number generator if it exists.
20+if hasattr(random, 'SystemRandom'):
21+    randrange = random.SystemRandom().randrange
22+else:
23+    randrange = random.randrange
24+_MAX_CSRF_KEY = 18446744073709551616L     # 2 << 63
25 
26+def _get_new_csrf_key():
27+    return md5_constructor("%s%s"
28+                % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
29+
30+def _make_token(csrf_cookie):
31+    return md5_constructor("csrf-" + settings.SECRET_KEY + csrf_cookie).hexdigest()
32+
33+def _cookie_name():
34+    # Backwards-compatibility: derive the cookie name from SESSION_COOKIE_NAME, so if the
35+    # user has specified a unique session cookie, we use a unique CSRF cookie, too.
36+    return "csrf_%s" % settings.SESSION_COOKIE_NAME
37+
38+def get_csrf_token(request):
39+    """Return the CSRF token to be inserted into forms by CsrfResponseMiddleware."""
40+    csrf_cookie = request.META.get("CSRF_COOKIE", None)
41+    assert csrf_cookie is not None, 'META["CSRF_COOKIE"] is not set.'
42+
43+    return _make_token(csrf_cookie)
44+
45 class CsrfViewMiddleware(object):
46     """
47     Middleware that requires a present and correct csrfmiddlewaretoken
48-    for POST requests that have an active session.
49+    for POST requests that have a CSRF cookie.
50     """
51     def process_view(self, request, callback, callback_args, callback_kwargs):
52+        if getattr(callback, 'csrf_exempt', False):
53+            return None
54+
55+        # If the user doesn't have a CSRF cookie, generate one and store it in the
56+        # request, so it's available to the view.  We'll store it in a cookie when
57+        # we reach the response.
58+        try:
59+            request.META["CSRF_COOKIE"] = request.COOKIES[_cookie_name()]
60+            cookie_is_new = False
61+        except KeyError:
62+            # No cookie, so create one.
63+            request.META["CSRF_COOKIE"] = _get_new_csrf_key()
64+            cookie_is_new = True
65+
66         if request.method == 'POST':
67-            if getattr(callback, 'csrf_exempt', False):
68-                return None
69-
70             if request.is_ajax():
71                 return None
72 
73-            try:
74-                session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
75-            except KeyError:
76-                # No session, no check required
77-                return None
78+            # If the user didn't already have a CSRF key, then accept the session key for the middleware
79+            # token, so CSRF protection isn't lost for the period between upgrading to CSRF cookies to
80+            # the first time each user comes back to the site to receive one.
81+            if cookie_is_new:
82+                try:
83+                    csrf_cookie = request.COOKIES[settings.SESSION_COOKIE_NAME]
84+                except KeyError:
85+                    # No CSRF cookie and no session cookie; no check is performed.
86+                    return None
87+            else:
88+                csrf_cookie = request.META["CSRF_COOKIE"]
89 
90-            csrf_token = _make_token(session_id)
91+            csrf_token = _make_token(csrf_cookie)
92             # check incoming token
93             try:
94                 request_csrf_token = request.POST['csrfmiddlewaretoken']
95@@ -60,35 +101,20 @@
96 
97 class CsrfResponseMiddleware(object):
98     """
99-    Middleware that post-processes a response to add a
100-    csrfmiddlewaretoken if the response/request have an active
101-    session.
102+    Middleware that post-processes a response to add a csrfmiddlewaretoken.
103     """
104     def process_response(self, request, response):
105         if getattr(response, 'csrf_exempt', False):
106             return response
107 
108-        csrf_token = None
109-        try:
110-            # This covers a corner case in which the outgoing response
111-            # both contains a form and sets a session cookie.  This
112-            # really should not be needed, since it is best if views
113-            # that create a new session (login pages) also do a
114-            # redirect, as is done by all such view functions in
115-            # Django.
116-            cookie = response.cookies[settings.SESSION_COOKIE_NAME]
117-            csrf_token = _make_token(cookie.value)
118-        except KeyError:
119-            # Normal case - look for existing session cookie
120-            try:
121-                session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
122-                csrf_token = _make_token(session_id)
123-            except KeyError:
124-                # no incoming or outgoing cookie
125-                pass
126+        # Set the CSRF cookie even if it's already set, so we renew the expiry timer.
127+        response.set_cookie(_cookie_name(),
128+                request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52,
129+                domain=settings.SESSION_COOKIE_DOMAIN,
130+                path=settings.SESSION_COOKIE_PATH)
131 
132-        if csrf_token is not None and \
133-                response['Content-Type'].split(';')[0] in _HTML_TYPES:
134+        if response['Content-Type'].split(';')[0] in _HTML_TYPES:
135+            csrf_token = get_csrf_token(request)
136 
137             # ensure we don't add the 'id' attribute twice (HTML validity)
138             idattributes = itertools.chain(("id='csrfmiddlewaretoken'",),
139@@ -109,18 +135,13 @@
140     Request Forgeries by adding hidden form fields to POST forms and
141     checking requests for the correct value.
142 
143-    In the list of middlewares, SessionMiddleware is required, and
144-    must come after this middleware.  CsrfMiddleWare must come after
145-    compression middleware.
146+    CsrfMiddleWare must come after compression middleware.
147 
148-    If a session ID cookie is present, it is hashed with the
149+    A persistent ID cookie is created.  This cookie is hashed with the
150     SECRET_KEY setting to create an authentication token.  This token
151     is added to all outgoing POST forms and is expected on all
152-    incoming POST requests that have a session ID cookie.
153+    incoming POST requests that have a CSRF ID cookie.
154 
155-    If you are setting cookies directly, instead of using Django's
156-    session framework, this middleware will not work.
157-
158     CsrfMiddleWare is composed of two middleware, CsrfViewMiddleware
159     and CsrfResponseMiddleware which can be used independently.
160     """
161Index: django/contrib/csrf/tests.py
162===================================================================
163--- django/contrib/csrf/tests.py        (revision 10369)
164+++ django/contrib/csrf/tests.py        (working copy)
165@@ -2,7 +2,7 @@
166 
167 from django.test import TestCase
168 from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
169-from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, csrf_exempt
170+from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, _cookie_name, csrf_exempt
171 from django.conf import settings
172 
173 
174@@ -17,120 +17,157 @@
175 
176 class CsrfMiddlewareTest(TestCase):
177 
178-    _session_id = "1"
179+    _csrf_id = "1"
180 
181-    def _get_GET_no_session_request(self):
182+    def _get_GET_no_csrf_cookie_request(self):
183         return HttpRequest()
184 
185-    def _get_GET_session_request(self):
186-        req = self._get_GET_no_session_request()
187-        req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
188+    def _get_GET_csrf_cookie_request(self):
189+        req = self._get_GET_no_csrf_cookie_request()
190+        req.COOKIES[_cookie_name()] = self._csrf_id
191         return req
192 
193-    def _get_POST_session_request(self):
194-        req = self._get_GET_session_request()
195+    def _get_POST_no_cookie_request(self):
196+        req = self._get_GET_no_csrf_cookie_request()
197         req.method = "POST"
198         return req
199 
200-    def _get_POST_no_session_request(self):
201-        req = self._get_GET_no_session_request()
202+    def _get_POST_csrf_cookie_request(self):
203+        req = self._get_GET_csrf_cookie_request()
204         req.method = "POST"
205         return req
206 
207+    def _get_POST_request_with_token(self):
208+        req = self._get_POST_csrf_cookie_request()
209+        req.POST['csrfmiddlewaretoken'] = _make_token(self._csrf_id)
210+        return req
211+
212     def _get_POST_session_request_with_token(self):
213-        req = self._get_POST_session_request()
214-        req.POST['csrfmiddlewaretoken'] = _make_token(self._session_id)
215+        req = self._get_POST_no_cookie_request()
216+        req.COOKIES[settings.SESSION_COOKIE_NAME] = self._csrf_id
217+        req.POST['csrfmiddlewaretoken'] = _make_token(self._csrf_id)
218         return req
219 
220+    def _get_POST_session_request_no_token(self):
221+        req = self._get_POST_no_cookie_request()
222+        req.COOKIES[settings.SESSION_COOKIE_NAME] = self._csrf_id
223+        return req
224+
225     def _get_post_form_response(self):
226         return post_form_response()
227 
228-    def _get_new_session_response(self):
229-        resp = self._get_post_form_response()
230-        resp.cookies[settings.SESSION_COOKIE_NAME] = self._session_id
231+    def _get_post_form_response_non_html(self):
232+        resp = post_form_response()
233+        resp["Content-Type"] = "application/xml"
234         return resp
235 
236-    def _check_token_present(self, response):
237-        self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(self._session_id))
238+    def _check_token_present(self, response, csrf_id=None):
239+        self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(csrf_id or self._csrf_id))
240 
241     def get_view(self):
242         return test_view
243 
244     # Check the post processing
245-    def test_process_response_no_session(self):
246+    def test_process_response_existing_cookie(self):
247         """
248-        Check the the post-processor does nothing if no session active
249+        Check that the token is inserted when a prior CSRF cookie exists.
250         """
251-        req = self._get_GET_no_session_request()
252+        req = self._get_GET_csrf_cookie_request()
253+        CsrfMiddleware().process_view(req, self.get_view(), (), {})
254+
255         resp = self._get_post_form_response()
256         resp_content = resp.content # needed because process_response modifies resp
257         resp2 = CsrfMiddleware().process_response(req, resp)
258-        self.assertEquals(resp_content, resp2.content)
259+        self.assertNotEqual(resp_content, resp2.content)
260+        self._check_token_present(resp2)
261 
262-    def test_process_response_existing_session(self):
263+    def test_process_response_existing_no_cookie(self):
264         """
265-        Check that the token is inserted if there is an existing session
266+        When no prior CSRF cookie exists, check that the cookie is created and a
267+        token is inserted.
268         """
269-        req = self._get_GET_session_request()
270+        req = self._get_GET_no_csrf_cookie_request()
271+        CsrfMiddleware().process_view(req, self.get_view(), (), {})
272+
273         resp = self._get_post_form_response()
274         resp_content = resp.content # needed because process_response modifies resp
275         resp2 = CsrfMiddleware().process_response(req, resp)
276+
277+        csrf_id = resp2.cookies.get(_cookie_name(), False)
278+        self.assertNotEqual(csrf_id, False)
279         self.assertNotEqual(resp_content, resp2.content)
280-        self._check_token_present(resp2)
281+        self._check_token_present(resp2, csrf_id.value)
282 
283-    def test_process_response_new_session(self):
284+    def test_process_response_no_cookie(self):
285         """
286-        Check that the token is inserted if there is a new session being started
287+        Check the the post-processor does nothing for content-types not in _HTML_TYPES.
288         """
289-        req = self._get_GET_no_session_request() # no session in request
290-        resp = self._get_new_session_response() # but new session started
291+        req = self._get_GET_no_csrf_cookie_request()
292+        CsrfMiddleware().process_view(req, self.get_view(), (), {})
293+        resp = self._get_post_form_response_non_html()
294         resp_content = resp.content # needed because process_response modifies resp
295         resp2 = CsrfMiddleware().process_response(req, resp)
296-        self.assertNotEqual(resp_content, resp2.content)
297-        self._check_token_present(resp2)
298+        self.assertEquals(resp_content, resp2.content)
299 
300     def test_process_response_exempt_view(self):
301         """
302         Check that no post processing is done for an exempt view
303         """
304-        req = self._get_POST_session_request()
305+        req = self._get_POST_csrf_cookie_request()
306         resp = csrf_exempt(self.get_view())(req)
307         resp_content = resp.content
308         resp2 = CsrfMiddleware().process_response(req, resp)
309         self.assertEquals(resp_content, resp2.content)
310 
311     # Check the request processing
312-    def test_process_request_no_session(self):
313+    def test_process_request_session_cookie_no_csrf_cookie_token(self):
314         """
315-        Check that if no session is present, the middleware does nothing.
316-        to the incoming request.
317+        When no CSRF cookie exists, but the user has a session, check that a token
318+        using the session cookie as the CSRF cookie is accepted.
319         """
320-        req = self._get_POST_no_session_request()
321+        req = self._get_POST_session_request_with_token()
322         req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
323         self.assertEquals(None, req2)
324+    def test_process_request_session_cookie_no_csrf_cookie_no_token(self):
325+        """
326+        When no CSRF cookie exists, but the user has a session, check that a token
327+        using the session cookie as the CSRF cookie is accepted.
328+        """
329+        req = self._get_POST_session_request_no_token()
330+        req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
331+        self.assertEquals(HttpResponseForbidden, req2.__class__)
332 
333-    def test_process_request_session_no_token(self):
334+    def test_process_request_no_cookie_no_token(self):
335         """
336-        Check that if a session is present but no token, we get a 'forbidden'
337+        Check that if no CSRF cookie is present, the middleware does nothing to
338+        the incoming request.
339         """
340-        req = self._get_POST_session_request()
341+        req = self._get_POST_no_cookie_request()
342         req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
343+        self.assertEquals(None, req2)
344+
345+    def test_process_request_cookie_no_token(self):
346+        """
347+        Check that if a cookie is present but no token, we get a 'forbidden'
348+        """
349+        req = self._get_POST_csrf_cookie_request()
350+        req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
351         self.assertEquals(HttpResponseForbidden, req2.__class__)
352 
353-    def test_process_request_session_and_token(self):
354+    def test_process_request_cookie_and_token(self):
355         """
356-        Check that if a session is present and a token, the middleware lets it through
357+        Check that if both a cookie and a token is present, the middleware lets it through.
358         """
359-        req = self._get_POST_session_request_with_token()
360+        req = self._get_POST_request_with_token()
361         req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
362         self.assertEquals(None, req2)
363 
364-    def test_process_request_session_no_token_exempt_view(self):
365+    def test_process_request_cookie_no_token_exempt_view(self):
366         """
367-        Check that if a session is present and no token, but the csrf_exempt
368+        Check that if a cookie is present and no token, but the csrf_exempt
369         decorator has been applied to the view, the middleware lets it through
370         """
371-        req = self._get_POST_session_request()
372+        req = self._get_POST_csrf_cookie_request()
373         req2 = CsrfMiddleware().process_view(req, csrf_exempt(self.get_view()), (), {})
374         self.assertEquals(None, req2)
375 
376@@ -138,7 +175,7 @@
377         """
378         Check that AJAX requests are automatically exempted.
379         """
380-        req = self._get_POST_session_request()
381+        req = self._get_POST_csrf_cookie_request()
382         req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
383         req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
384         self.assertEquals(None, req2)
385Index: docs/ref/contrib/csrf.txt
386===================================================================
387--- docs/ref/contrib/csrf.txt   (revision 10369)
388+++ docs/ref/contrib/csrf.txt   (working copy)
389@@ -23,11 +23,9 @@
390 =============
391 
392 Add the middleware ``'django.contrib.csrf.middleware.CsrfMiddleware'`` to
393-your list of middleware classes, :setting:`MIDDLEWARE_CLASSES`. It needs to process
394-the response after the SessionMiddleware, so must come before it in the
395-list. It also must process the response before things like compression
396-happen to the response, so it must come after GZipMiddleware in the
397-list.
398+your list of middleware classes, :setting:`MIDDLEWARE_CLASSES`. It must
399+process the response before things like compression happen to the response,
400+so it must come after GZipMiddleware in the list.
401 
402 The ``CsrfMiddleware`` class is actually composed of two middleware:
403 ``CsrfViewMiddleware`` which performs the checks on incoming requests,
404@@ -70,17 +68,17 @@
405 
406 CsrfMiddleware does two things:
407 
408-1. It modifies outgoing requests by adding a hidden form field to all
409+1. It stores a random number in a cookie.  For backwards-compatibility
410+   purposes, the cookie name is derived from the SESSION_COOKIE_NAME setting.
411+
412+2. It modifies outgoing requests by adding a hidden form field to all
413    'POST' forms, with the name 'csrfmiddlewaretoken' and a value which is
414-   a hash of the session ID plus a secret. If there is no session ID set,
415-   this modification of the response isn't done, so there is very little
416-   performance penalty for those requests that don't have a session.
417-   (This is done by ``CsrfResponseMiddleware``).
418+   a hash of the cookie value plus a secret.  (This is done by
419+   ``CsrfResponseMiddleware``).
420 
421-2. On all incoming POST requests that have the session cookie set, it
422-   checks that the 'csrfmiddlewaretoken' is present and correct. If it
423-   isn't, the user will get a 403 error. (This is done by
424-   ``CsrfViewMiddleware``)
425+3. On all incoming POST requests that have the cookie set, it checks that
426+   the 'csrfmiddlewaretoken' is present and correct. If it isn't, the user
427+   will get a 403 error. (This is done by ``CsrfViewMiddleware``)
428 
429 This ensures that only forms that have originated from your Web site
430 can be used to POST data back.
431@@ -90,10 +88,6 @@
432 effects (see `9.1.1 Safe Methods, HTTP 1.1, RFC 2616`_), and so a
433 CSRF attack with a GET request ought to be harmless.
434 
435-POST requests that are not accompanied by a session cookie are not protected,
436-but they do not need to be protected, since the 'attacking' Web site
437-could make these kind of requests anyway.
438-
439 The Content-Type is checked before modifying the response, and only
440 pages that are served as 'text/html' or 'application/xml+xhtml'
441 are modified.
442@@ -112,10 +106,6 @@
443 Limitations
444 ===========
445 
446-CsrfMiddleware requires Django's session framework to work. If you have
447-a custom authentication system that manually sets cookies and the like,
448-it won't help you.
449-
450 If your app creates HTML pages and forms in some unusual way, (e.g.
451 it sends fragments of HTML in JavaScript document.write statements)
452 you might bypass the filter that adds the hidden field to the form,