From 9a2e33810789017dfb2cbe46afa01f3f45357757 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Sat, 7 Feb 2009 17:47:02 +0000 Subject: [PATCH] Made CSRF middleware skip post-processing for 'csrf_exempt' decorated views. This commit also decomposes the decorator into two decorators which can be used separately, adds some tests, updates docs and fixes some code comments. git-svn-id: http://code.djangoproject.com/svn/django/trunk@9815 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/csrf/middleware.py | 39 +++++++++++++++++++++++++------ django/contrib/csrf/tests.py | 18 ++++++++++---- docs/ref/contrib/csrf.txt | 35 +++++++++++++++++---------- 3 files changed, 69 insertions(+), 23 deletions(-) diff --git a/django/contrib/csrf/middleware.py b/django/contrib/csrf/middleware.py index 3a06feb398..30cb8caa5e 100644 --- a/django/contrib/csrf/middleware.py +++ b/django/contrib/csrf/middleware.py @@ -65,9 +65,12 @@ class CsrfResponseMiddleware(object): session. """ def process_response(self, request, response): + if getattr(response, 'csrf_exempt', False): + return response + csrf_token = None try: - # This covers a corner case in which the outgoing request + # This covers a corner case in which the outgoing response # both contains a form and sets a session cookie. This # really should not be needed, since it is best if views # that create a new session (login pages) also do a @@ -123,13 +126,35 @@ class CsrfMiddleware(CsrfViewMiddleware, CsrfResponseMiddleware): """ pass +def csrf_response_exempt(view_func): + """ + Modifies a view function so that its response is exempt + from the post-processing of the CSRF middleware. + """ + def wrapped_view(*args, **kwargs): + resp = view_func(*args, **kwargs) + resp.csrf_exempt = True + return resp + return wraps(view_func)(wrapped_view) + +def csrf_view_exempt(view_func): + """ + Marks a view function as being exempt from CSRF view protection. + """ + # We could just do view_func.csrf_exempt = True, but decorators + # are nicer if they don't have side-effects, so we return a new + # function. + def wrapped_view(*args, **kwargs): + return view_func(*args, **kwargs) + wrapped_view.csrf_exempt = True + return wraps(view_func)(wrapped_view) + def csrf_exempt(view_func): """ Marks a view function as being exempt from the CSRF checks + and post processing. + + This is the same as using both the csrf_exempt_view and + csrf_exempt_response decorators. """ - def wrapped_view(*args, **kwargs): - return view_func(*args, **kwargs) - # We could just do view.csrf_exempt = True, but decorators are - # nicer if they don't have side-effects. - wrapped_view.csrf_exempt = True - return wraps(view_func)(wrapped_view) + return csrf_response_exempt(csrf_view_exempt(view_func)) diff --git a/django/contrib/csrf/tests.py b/django/contrib/csrf/tests.py index b9f402e1a9..ba934bc7ea 100644 --- a/django/contrib/csrf/tests.py +++ b/django/contrib/csrf/tests.py @@ -63,7 +63,7 @@ class CsrfMiddlewareTest(TestCase): """ req = self._get_GET_no_session_request() resp = self._get_post_form_response() - resp_content = resp.content + resp_content = resp.content # needed because process_response modifies resp resp2 = CsrfMiddleware().process_response(req, resp) self.assertEquals(resp_content, resp2.content) @@ -73,7 +73,7 @@ class CsrfMiddlewareTest(TestCase): """ req = self._get_GET_session_request() resp = self._get_post_form_response() - resp_content = resp.content + resp_content = resp.content # needed because process_response modifies resp resp2 = CsrfMiddleware().process_response(req, resp) self.assertNotEqual(resp_content, resp2.content) self._check_token_present(resp2) @@ -84,11 +84,21 @@ class CsrfMiddlewareTest(TestCase): """ req = self._get_GET_no_session_request() # no session in request resp = self._get_new_session_response() # but new session started - resp_content = resp.content + resp_content = resp.content # needed because process_response modifies resp resp2 = CsrfMiddleware().process_response(req, resp) self.assertNotEqual(resp_content, resp2.content) self._check_token_present(resp2) + def test_process_response_exempt_view(self): + """ + Check that no post processing is done for an exempt view + """ + req = self._get_POST_session_request() + resp = csrf_exempt(self.get_view())(req) + resp_content = resp.content + resp2 = CsrfMiddleware().process_response(req, resp) + self.assertEquals(resp_content, resp2.content) + # Check the request processing def test_process_request_no_session(self): """ @@ -126,7 +136,7 @@ class CsrfMiddlewareTest(TestCase): def test_ajax_exemption(self): """ - Check the AJAX requests are automatically exempted. + Check that AJAX requests are automatically exempted. """ req = self._get_POST_session_request() req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index f89cb1503e..1b6b6102de 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -29,6 +29,16 @@ list. It also must process the response before things like compression happen to the response, so it must come after GZipMiddleware in the list. +The ``CsrfMiddleware`` class is actually composed of two middleware: +``CsrfViewMiddleware`` which performs the checks on incoming requests, +and ``CsrfResponseMiddleware`` which performs post-processing of the +result. This allows the individual components to be used and/or +replaced instead of using ``CsrfMiddleware``. + +.. versionchanged:: 1.1 + (previous versions of Django did not provide these two components + of ``CsrfMiddleware`` as described above) + Exceptions ---------- @@ -44,9 +54,16 @@ the ``django.contrib.csrf.middleware`` module. For example:: return HttpResponse('Hello world') my_view = csrf_exempt(my_view) -You don't have to worry about doing this for most AJAX views. Any request sent -with "X-Requested-With: XMLHttpRequest" is automatically exempt. (See the next -section.) +Like the middleware itself, the ``csrf_exempt`` decorator is composed +of two parts: a ``csrf_view_exempt`` decorator and a +``csrf_response_exempt`` decorator, found in the same module. These +disable the view protection mechanism (``CsrfViewMiddleware``) and the +response post-processing (``CsrfResponseMiddleware``) respectively. +They can be used individually if required. + +You don't have to worry about doing this for most AJAX views. Any +request sent with "X-Requested-With: XMLHttpRequest" is automatically +exempt. (See the next section.) How it works ============ @@ -58,10 +75,12 @@ CsrfMiddleware does two things: a hash of the session ID plus a secret. If there is no session ID set, this modification of the response isn't done, so there is very little performance penalty for those requests that don't have a session. + (This is done by ``CsrfResponseMiddleware``). 2. On all incoming POST requests that have the session cookie set, it checks that the 'csrfmiddlewaretoken' is present and correct. If it - isn't, the user will get a 403 error. + isn't, the user will get a 403 error. (This is done by + ``CsrfViewMiddleware``) This ensures that only forms that have originated from your Web site can be used to POST data back. @@ -87,14 +106,6 @@ be added by using ``XMLHttpRequest``, and browsers already implement a same-domain policy for ``XMLHttpRequest``. (Note that this is not secure if you don't trust content within the same domain or subdomains.) -The above two functions of ``CsrfMiddleware`` are split between two -classes: ``CsrfResponseMiddleware`` and ``CsrfViewMiddleware`` -respectively. This allows the individual components to be used and/or -replaced instead of using ``CsrfMiddleware``. - -.. versionchanged:: 1.1 - (previous versions of Django did not provide these two components - of ``CsrfMiddleware`` as described above) .. _9.1.1 Safe Methods, HTTP 1.1, RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html