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

File csrf-remove-session-dep-r10369.3.diff, 20.5 KB (added by Glenn, 6 years ago)

fix session cookie BC and revise test to catch this; fix exception when the view middleware is skipped

  • django/contrib/csrf/middleware.py

     
    77
    88import re
    99import itertools
     10import random
    1011try:
    1112    from functools import wraps
    1213except ImportError:
     
    2425
    2526_HTML_TYPES = ('text/html', 'application/xhtml+xml')
    2627
    27 def _make_token(session_id):
     28# Use the system (hardware-based) random number generator if it exists.
     29if hasattr(random, 'SystemRandom'):
     30    randrange = random.SystemRandom().randrange
     31else:
     32    randrange = random.randrange
     33_MAX_CSRF_KEY = 18446744073709551616L     # 2 << 63
     34
     35def _get_new_csrf_key():
     36    return md5_constructor("%s%s"
     37                % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
     38
     39def _make_legacy_session_token(session_id):
    2840    return md5_constructor(settings.SECRET_KEY + session_id).hexdigest()
    2941
     42def _make_token(csrf_cookie):
     43    return md5_constructor("csrf-" + settings.SECRET_KEY + csrf_cookie).hexdigest()
     44
     45def _cookie_name():
     46    # Backwards-compatibility: derive the cookie name from SESSION_COOKIE_NAME, so if the
     47    # user has specified a unique session cookie, we use a unique CSRF cookie, too.
     48    return "csrf_%s" % settings.SESSION_COOKIE_NAME
     49
     50def get_csrf_token(request):
     51    """Return the CSRF token to be inserted into forms by CsrfResponseMiddleware."""
     52    csrf_cookie = request.META.get("CSRF_COOKIE", None)
     53    assert csrf_cookie is not None, 'META["CSRF_COOKIE"] is not set.'
     54
     55    return _make_token(csrf_cookie)
     56
    3057class CsrfViewMiddleware(object):
    3158    """
    3259    Middleware that requires a present and correct csrfmiddlewaretoken
    33     for POST requests that have an active session.
     60    for POST requests that have a CSRF cookie.
    3461    """
    3562    def process_view(self, request, callback, callback_args, callback_kwargs):
     63        if getattr(callback, 'csrf_exempt', False):
     64            return None
     65
     66        # If the user doesn't have a CSRF cookie, generate one and store it in the
     67        # request, so it's available to the view.  We'll store it in a cookie when
     68        # we reach the response.
     69        try:
     70            request.META["CSRF_COOKIE"] = request.COOKIES[_cookie_name()]
     71            cookie_is_new = False
     72        except KeyError:
     73            # No cookie, so create one.
     74            request.META["CSRF_COOKIE"] = _get_new_csrf_key()
     75            cookie_is_new = True
     76
    3677        if request.method == 'POST':
    37             if getattr(callback, 'csrf_exempt', False):
    38                 return None
    39 
    4078            if request.is_ajax():
    4179                return None
    4280
    43             try:
    44                 session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
    45             except KeyError:
    46                 # No session, no check required
    47                 return None
     81            # If the user didn't already have a CSRF key, then accept the session key for the middleware
     82            # token, so CSRF protection isn't lost for the period between upgrading to CSRF cookies to
     83            # the first time each user comes back to the site to receive one.
     84            if cookie_is_new:
     85                try:
     86                    session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
     87                    csrf_token = _make_legacy_session_token(session_id)
     88                except KeyError:
     89                    # No CSRF cookie and no session cookie; no check is performed.
     90                    return None
     91            else:
     92                csrf_cookie = request.META["CSRF_COOKIE"]
     93                csrf_token = _make_token(csrf_cookie)
    4894
    49             csrf_token = _make_token(session_id)
    5095            # check incoming token
    5196            try:
    5297                request_csrf_token = request.POST['csrfmiddlewaretoken']
     
    60105
    61106class CsrfResponseMiddleware(object):
    62107    """
    63     Middleware that post-processes a response to add a
    64     csrfmiddlewaretoken if the response/request have an active
    65     session.
     108    Middleware that post-processes a response to add a csrfmiddlewaretoken.
    66109    """
    67110    def process_response(self, request, response):
    68111        if getattr(response, 'csrf_exempt', False):
    69112            return response
    70113
    71         csrf_token = None
    72         try:
    73             # This covers a corner case in which the outgoing response
    74             # both contains a form and sets a session cookie.  This
    75             # really should not be needed, since it is best if views
    76             # that create a new session (login pages) also do a
    77             # redirect, as is done by all such view functions in
    78             # Django.
    79             cookie = response.cookies[settings.SESSION_COOKIE_NAME]
    80             csrf_token = _make_token(cookie.value)
    81         except KeyError:
    82             # Normal case - look for existing session cookie
    83             try:
    84                 session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
    85                 csrf_token = _make_token(session_id)
    86             except KeyError:
    87                 # no incoming or outgoing cookie
    88                 pass
     114        # If CSRF_COOKIE is unset, then CsrfViewMiddleware.process_view was never called, probably
     115        # because a request middleware returned a response (for example, contrib.auth redirecting
     116        # to a login page).
     117        if request.META.get("CSRF_COOKIE") is None:
     118            return response
    89119
    90         if csrf_token is not None and \
    91                 response['Content-Type'].split(';')[0] in _HTML_TYPES:
     120        # Set the CSRF cookie even if it's already set, so we renew the expiry timer.
     121        response.set_cookie(_cookie_name(),
     122                request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52,
     123                domain=settings.SESSION_COOKIE_DOMAIN,
     124                path=settings.SESSION_COOKIE_PATH)
    92125
     126        if response['Content-Type'].split(';')[0] in _HTML_TYPES:
     127            csrf_token = get_csrf_token(request)
     128
    93129            # ensure we don't add the 'id' attribute twice (HTML validity)
    94130            idattributes = itertools.chain(("id='csrfmiddlewaretoken'",),
    95131                                            itertools.repeat(''))
     
    109145    Request Forgeries by adding hidden form fields to POST forms and
    110146    checking requests for the correct value.
    111147
    112     In the list of middlewares, SessionMiddleware is required, and
    113     must come after this middleware.  CsrfMiddleWare must come after
    114     compression middleware.
     148    CsrfMiddleWare must come after compression middleware.
    115149
    116     If a session ID cookie is present, it is hashed with the
     150    A persistent ID cookie is created.  This cookie is hashed with the
    117151    SECRET_KEY setting to create an authentication token.  This token
    118152    is added to all outgoing POST forms and is expected on all
    119     incoming POST requests that have a session ID cookie.
     153    incoming POST requests that have a CSRF ID cookie.
    120154
    121     If you are setting cookies directly, instead of using Django's
    122     session framework, this middleware will not work.
    123 
    124155    CsrfMiddleWare is composed of two middleware, CsrfViewMiddleware
    125156    and CsrfResponseMiddleware which can be used independently.
    126157    """
  • django/contrib/csrf/tests.py

     
    22
    33from django.test import TestCase
    44from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
    5 from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, csrf_exempt
     5from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, _cookie_name, csrf_exempt
    66from django.conf import settings
    77
    88
     
    1717
    1818class CsrfMiddlewareTest(TestCase):
    1919
     20    _csrf_id = "1"
     21
     22    # This is a valid session token for this ID and secret key.  This was generated using
     23    # the old code that we're to be backwards-compatible with.  Don't use the CSRF code
     24    # to generate this hash, or we're merely testing the code against itself and not
     25    # checking backwards-compatibility.  This is also the output of (echo -n test1 | md5sum).
     26    _session_token = "5a105e8b9d40e1329780d62ea2265d8a"
    2027    _session_id = "1"
     28    _secret_key_for_session_test= "test"
    2129
    22     def _get_GET_no_session_request(self):
     30    def _get_GET_no_csrf_cookie_request(self):
    2331        return HttpRequest()
    2432
    25     def _get_GET_session_request(self):
    26         req = self._get_GET_no_session_request()
    27         req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
     33    def _get_GET_csrf_cookie_request(self):
     34        req = self._get_GET_no_csrf_cookie_request()
     35        req.COOKIES[_cookie_name()] = self._csrf_id
    2836        return req
    2937
    30     def _get_POST_session_request(self):
    31         req = self._get_GET_session_request()
     38    def _get_POST_no_cookie_request(self):
     39        req = self._get_GET_no_csrf_cookie_request()
    3240        req.method = "POST"
    3341        return req
    3442
    35     def _get_POST_no_session_request(self):
    36         req = self._get_GET_no_session_request()
     43    def _get_POST_csrf_cookie_request(self):
     44        req = self._get_GET_csrf_cookie_request()
    3745        req.method = "POST"
    3846        return req
    3947
     48    def _get_POST_request_with_token(self):
     49        req = self._get_POST_csrf_cookie_request()
     50        req.POST['csrfmiddlewaretoken'] = _make_token(self._csrf_id)
     51        return req
     52
    4053    def _get_POST_session_request_with_token(self):
    41         req = self._get_POST_session_request()
    42         req.POST['csrfmiddlewaretoken'] = _make_token(self._session_id)
     54        req = self._get_POST_no_cookie_request()
     55        req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
     56        req.POST['csrfmiddlewaretoken'] = self._session_token
    4357        return req
    4458
     59    def _get_POST_session_request_no_token(self):
     60        req = self._get_POST_no_cookie_request()
     61        req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
     62        return req
     63
    4564    def _get_post_form_response(self):
    4665        return post_form_response()
    4766
    48     def _get_new_session_response(self):
    49         resp = self._get_post_form_response()
    50         resp.cookies[settings.SESSION_COOKIE_NAME] = self._session_id
     67    def _get_post_form_response_non_html(self):
     68        resp = post_form_response()
     69        resp["Content-Type"] = "application/xml"
    5170        return resp
    5271
    53     def _check_token_present(self, response):
    54         self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(self._session_id))
     72    def _check_token_present(self, response, csrf_id=None):
     73        self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(csrf_id or self._csrf_id))
    5574
    5675    def get_view(self):
    5776        return test_view
    5877
    5978    # Check the post processing
    60     def test_process_response_no_session(self):
     79    def test_process_response_existing_cookie(self):
    6180        """
    62         Check the the post-processor does nothing if no session active
     81        Check that the token is inserted when a prior CSRF cookie exists.
    6382        """
    64         req = self._get_GET_no_session_request()
     83        req = self._get_GET_csrf_cookie_request()
     84        CsrfMiddleware().process_view(req, self.get_view(), (), {})
     85
    6586        resp = self._get_post_form_response()
    6687        resp_content = resp.content # needed because process_response modifies resp
    6788        resp2 = CsrfMiddleware().process_response(req, resp)
    68         self.assertEquals(resp_content, resp2.content)
     89        self.assertNotEqual(resp_content, resp2.content)
     90        self._check_token_present(resp2)
    6991
    70     def test_process_response_existing_session(self):
     92    def test_process_response_existing_no_cookie(self):
    7193        """
    72         Check that the token is inserted if there is an existing session
     94        When no prior CSRF cookie exists, check that the cookie is created and a
     95        token is inserted.
    7396        """
    74         req = self._get_GET_session_request()
     97        req = self._get_GET_no_csrf_cookie_request()
     98        CsrfMiddleware().process_view(req, self.get_view(), (), {})
     99
    75100        resp = self._get_post_form_response()
    76101        resp_content = resp.content # needed because process_response modifies resp
    77102        resp2 = CsrfMiddleware().process_response(req, resp)
     103
     104        csrf_id = resp2.cookies.get(_cookie_name(), False)
     105        self.assertNotEqual(csrf_id, False)
    78106        self.assertNotEqual(resp_content, resp2.content)
    79         self._check_token_present(resp2)
     107        self._check_token_present(resp2, csrf_id.value)
    80108
    81     def test_process_response_new_session(self):
     109    def test_process_response_no_cookie(self):
    82110        """
    83         Check that the token is inserted if there is a new session being started
     111        Check the the post-processor does nothing for content-types not in _HTML_TYPES.
    84112        """
    85         req = self._get_GET_no_session_request() # no session in request
    86         resp = self._get_new_session_response() # but new session started
     113        req = self._get_GET_no_csrf_cookie_request()
     114        CsrfMiddleware().process_view(req, self.get_view(), (), {})
     115        resp = self._get_post_form_response_non_html()
    87116        resp_content = resp.content # needed because process_response modifies resp
    88117        resp2 = CsrfMiddleware().process_response(req, resp)
    89         self.assertNotEqual(resp_content, resp2.content)
    90         self._check_token_present(resp2)
     118        self.assertEquals(resp_content, resp2.content)
    91119
    92120    def test_process_response_exempt_view(self):
    93121        """
    94122        Check that no post processing is done for an exempt view
    95123        """
    96         req = self._get_POST_session_request()
     124        req = self._get_POST_csrf_cookie_request()
    97125        resp = csrf_exempt(self.get_view())(req)
    98126        resp_content = resp.content
    99127        resp2 = CsrfMiddleware().process_response(req, resp)
    100128        self.assertEquals(resp_content, resp2.content)
    101129
    102130    # Check the request processing
    103     def test_process_request_no_session(self):
     131    def test_process_request_no_cookie_no_token(self):
    104132        """
    105         Check that if no session is present, the middleware does nothing.
    106         to the incoming request.
     133        Check that if neither a CSRF cookie nor a session cookie are present, the
     134        middleware does nothing to the incoming request.
    107135        """
    108         req = self._get_POST_no_session_request()
     136        req = self._get_POST_no_cookie_request()
    109137        req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
    110138        self.assertEquals(None, req2)
    111139
    112     def test_process_request_session_no_token(self):
     140    def test_process_request_cookie_no_token(self):
    113141        """
    114         Check that if a session is present but no token, we get a 'forbidden'
     142        Check that if a CSRF cookie is present but no token, we get a 'forbidden'.
    115143        """
    116         req = self._get_POST_session_request()
     144        req = self._get_POST_csrf_cookie_request()
    117145        req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
    118146        self.assertEquals(HttpResponseForbidden, req2.__class__)
    119147
    120     def test_process_request_session_and_token(self):
     148    def test_process_request_cookie_and_token(self):
    121149        """
    122         Check that if a session is present and a token, the middleware lets it through
     150        Check that if both a cookie and a token is present, the middleware lets it through.
    123151        """
    124         req = self._get_POST_session_request_with_token()
     152        req = self._get_POST_request_with_token()
    125153        req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
    126154        self.assertEquals(None, req2)
    127155
    128     def test_process_request_session_no_token_exempt_view(self):
     156    def test_process_request_session_cookie_no_csrf_cookie_token(self):
    129157        """
    130         Check that if a session is present and no token, but the csrf_exempt
     158        When no CSRF cookie exists, but the user has a session, check that a token
     159        using the session cookie as a legacy CSRF cookie is accepted.
     160        """
     161        settings.SECRET_KEY = self._secret_key_for_session_test
     162        try:
     163            req = self._get_POST_session_request_with_token()
     164            req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
     165            self.assertEquals(None, req2)
     166        finally:
     167            orig_secret_key = settings.SECRET_KEY
     168
     169    def test_process_request_session_cookie_no_csrf_cookie_no_token(self):
     170        """
     171        Check that if a session cookie is present but no token and no CSRF cookie,
     172        we get a 'forbidden'.
     173        """
     174        req = self._get_POST_session_request_no_token()
     175        req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
     176        self.assertEquals(HttpResponseForbidden, req2.__class__)
     177
     178    def test_process_request_cookie_no_token_exempt_view(self):
     179        """
     180        Check that if a cookie is present and no token, but the csrf_exempt
    131181        decorator has been applied to the view, the middleware lets it through
    132182        """
    133         req = self._get_POST_session_request()
     183        req = self._get_POST_csrf_cookie_request()
    134184        req2 = CsrfMiddleware().process_view(req, csrf_exempt(self.get_view()), (), {})
    135185        self.assertEquals(None, req2)
    136186
     
    138188        """
    139189        Check that AJAX requests are automatically exempted.
    140190        """
    141         req = self._get_POST_session_request()
     191        req = self._get_POST_csrf_cookie_request()
    142192        req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
    143193        req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
    144194        self.assertEquals(None, req2)
  • docs/ref/contrib/csrf.txt

     
    2323=============
    2424
    2525Add the middleware ``'django.contrib.csrf.middleware.CsrfMiddleware'`` to
    26 your list of middleware classes, :setting:`MIDDLEWARE_CLASSES`. It needs to process
    27 the response after the SessionMiddleware, so must come before it in the
    28 list. It also must process the response before things like compression
    29 happen to the response, so it must come after GZipMiddleware in the
    30 list.
     26your list of middleware classes, :setting:`MIDDLEWARE_CLASSES`. It must
     27process the response before things like compression happen to the response,
     28so it must come after GZipMiddleware in the list.
    3129
    3230The ``CsrfMiddleware`` class is actually composed of two middleware:
    3331``CsrfViewMiddleware`` which performs the checks on incoming requests,
     
    7068
    7169CsrfMiddleware does two things:
    7270
    73 1. It modifies outgoing requests by adding a hidden form field to all
     711. It stores a random number in a cookie.  For backwards-compatibility
     72   purposes, the cookie name is derived from the SESSION_COOKIE_NAME setting.
     73
     742. It modifies outgoing requests by adding a hidden form field to all
    7475   'POST' forms, with the name 'csrfmiddlewaretoken' and a value which is
    75    a hash of the session ID plus a secret. If there is no session ID set,
    76    this modification of the response isn't done, so there is very little
    77    performance penalty for those requests that don't have a session.
    78    (This is done by ``CsrfResponseMiddleware``).
     76   a hash of the cookie value plus a secret.  (This is done by
     77   ``CsrfResponseMiddleware``).
    7978
    80 2. On all incoming POST requests that have the session cookie set, it
    81    checks that the 'csrfmiddlewaretoken' is present and correct. If it
    82    isn't, the user will get a 403 error. (This is done by
    83    ``CsrfViewMiddleware``)
     793. On all incoming POST requests that have the cookie set, it checks that
     80   the 'csrfmiddlewaretoken' is present and correct. If it isn't, the user
     81   will get a 403 error. (This is done by ``CsrfViewMiddleware``)
    8482
    8583This ensures that only forms that have originated from your Web site
    8684can be used to POST data back.
     
    9088effects (see `9.1.1 Safe Methods, HTTP 1.1, RFC 2616`_), and so a
    9189CSRF attack with a GET request ought to be harmless.
    9290
    93 POST requests that are not accompanied by a session cookie are not protected,
    94 but they do not need to be protected, since the 'attacking' Web site
    95 could make these kind of requests anyway.
    96 
    9791The Content-Type is checked before modifying the response, and only
    9892pages that are served as 'text/html' or 'application/xml+xhtml'
    9993are modified.
     
    112106Limitations
    113107===========
    114108
    115 CsrfMiddleware requires Django's session framework to work. If you have
    116 a custom authentication system that manually sets cookies and the like,
    117 it won't help you.
    118 
    119109If your app creates HTML pages and forms in some unusual way, (e.g.
    120110it sends fragments of HTML in JavaScript document.write statements)
    121111you might bypass the filter that adds the hidden field to the form,
Back to Top