diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py index c9fd82ceed..9b92d26e95 100644 --- a/django/views/decorators/csrf.py +++ b/django/views/decorators/csrf.py @@ -1,6 +1,6 @@ import warnings -from django.middleware.csrf import CsrfViewMiddleware +from django.middleware.csrf import CsrfViewMiddleware, get_token from django.utils.decorators import decorator_from_middleware, available_attrs from functools import wraps @@ -28,6 +28,26 @@ RequestContext, but without the CSRF protection that csrf_protect enforces. """ + +class _EnsureCsrfCookie(CsrfViewMiddleware): + def _reject(self, request, reason): + return None + + def process_view(self, request, callback, callback_args, callback_kwargs): + retval = super(_EnsureCsrfCookie, self).process_view(request, callback, callback_args, callback_kwargs) + # Forces process_response to send the cookie + get_token(request) + return retval + + +ensure_csrf_cookie = decorator_from_middleware(_EnsureCsrfCookie) +ensure_csrf_cookie.__name__ = 'ensure_csrf_cookie' +ensure_csrf_cookie.__doc__ = """ +Use this decorator to ensure that a view sets a CSRF cookie, whether or not it +uses the csrf_token template tag, or the CsrfViewMiddleware is used. +""" + + def csrf_response_exempt(view_func): """ Modifies a view function so that its response is exempt diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 8acab718d3..7ff7d53aa0 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -132,6 +132,10 @@ The above code could be simplified by using the `jQuery cookie plugin `settings.crossDomain `_ in jQuery 1.5 and later to replace ``sameOrigin``. +In addition, if the CSRF cookie has not been sent to the client by use of +:ttag:`csrf_token`, you may need to ensure the client receives the cookie by +using :func:`~django.views.decorators.csrf.ensure_csrf_cookie`. + The decorator method -------------------- @@ -328,6 +332,10 @@ Utilities # ... return render(request, "a_template.html", c) +.. function:: ensure_csrf_cookie(view) + + This decorator forces a view to send the CSRF cookie. + Scenarios --------- @@ -381,6 +389,15 @@ path within it that needs protection. Example:: else: do_something_else() +Page uses AJAX without any HTML form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A page makes a POST request via AJAX, and the page does not have an HTML form +with a :ttag:`csrf_token` that would cause the required CSRF cookie to be sent. + +Solution: use :func:`~django.views.decorators.csrf.ensure_csrf_cookie` on the +view that sends the page. + Contrib and reusable apps ========================= diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py index a98a6a4282..1285c1ddac 100644 --- a/tests/regressiontests/csrf_tests/tests.py +++ b/tests/regressiontests/csrf_tests/tests.py @@ -4,7 +4,7 @@ import warnings from django.test import TestCase from django.http import HttpRequest, HttpResponse from django.middleware.csrf import CsrfViewMiddleware -from django.views.decorators.csrf import csrf_exempt, requires_csrf_token +from django.views.decorators.csrf import csrf_exempt, requires_csrf_token, ensure_csrf_cookie from django.core.context_processors import csrf from django.conf import settings from django.template import RequestContext, Template @@ -249,3 +249,35 @@ 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_ensures_csrf_cookie_no_middleware(self): + """ + Tests that ensures_csrf_cookie decorator fulfils its promise + with no middleware + """ + @ensure_csrf_cookie + def view(request): + # Doesn't insert a token or anything + return HttpResponse(content="") + + req = self._get_GET_no_csrf_cookie_request() + resp = view(req) + self.assertTrue(resp.cookies.get(settings.CSRF_COOKIE_NAME, False)) + self.assertTrue('Cookie' in resp.get('Vary','')) + + def test_ensures_csrf_cookie_with_middleware(self): + """ + Tests that ensures_csrf_cookie decorator fulfils its promise + with the middleware enabled. + """ + @ensure_csrf_cookie + def view(request): + # Doesn't insert a token or anything + return HttpResponse(content="") + + req = self._get_GET_no_csrf_cookie_request() + CsrfViewMiddleware().process_view(req, view, (), {}) + resp = view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + self.assertTrue(resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)) + self.assertTrue('Cookie' in resp2.get('Vary',''))