From bfe654f0af480473531bdff77cefb676568aac8f Mon Sep 17 00:00:00 2001 From: Luke Cyca Date: Thu, 30 May 2013 10:25:58 -0700 Subject: Support CSRF via X-CSRFToken Header --- pyramid/session.py | 26 +++++++++++++++++--------- pyramid/tests/test_session.py | 21 +++++++++++++++------ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/pyramid/session.py b/pyramid/session.py index 7db8c8e0e..0433488d8 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -81,15 +81,22 @@ def signed_deserialize(serialized, secret, hmac=hmac): return pickle.loads(pickled) -def check_csrf_token(request, token='csrf_token', raises=True): +def check_csrf_token(request, + token='csrf_token', + header='X-CSRFToken', + raises=True): """ Check the CSRF token in the request's session against the value in - ``request.params.get(token)``. If a ``token`` keyword is not supplied - to this function, the string ``csrf_token`` will be used to look up - the token within ``request.params``. If the value in - ``request.params.get(token)`` doesn't match the value supplied by - ``request.session.get_csrf_token()``, and ``raises`` is ``True``, this - function will raise an :exc:`pyramid.httpexceptions.HTTPBadRequest` - exception. If the check does succeed and ``raises`` is ``False``, this + ``request.params.get(token)`` or ``request.headers.get(header)``. + If a ``token`` keyword is not supplied to this function, the string + ``csrf_token`` will be used to look up the token in ``request.params``. + If a ``header`` keyword is not supplied to this function, the string + ``X-CSRFToken`` will be used to look up the token in ``request.headers``. + + If the value supplied by param or by header doesn't match the value + supplied by ``request.session.get_csrf_token()``, and ``raises`` is + ``True``, this function will raise an + :exc:`pyramid.httpexceptions.HTTPBadRequest` exception. + If the check does succeed and ``raises`` is ``False``, this function will return ``False``. If the CSRF check is successful, this function will return ``True`` unconditionally. @@ -98,7 +105,8 @@ def check_csrf_token(request, token='csrf_token', raises=True): .. versionadded:: 1.4a2 """ - if request.params.get(token) != request.session.get_csrf_token(): + supplied_token = request.params.get(token, request.headers.get(header)) + if supplied_token != request.session.get_csrf_token(): if raises: raise HTTPBadRequest('incorrect CSRF token') return False diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index b3e0e20c4..d3bafb26e 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -356,20 +356,29 @@ class Test_signed_deserialize(unittest.TestCase): self.assertRaises(ValueError, self._callFUT, serialized, 'secret') class Test_check_csrf_token(unittest.TestCase): - def _callFUT(self, request, token, raises=True): + def _callFUT(self, *args, **kwargs): from ..session import check_csrf_token - return check_csrf_token(request, token, raises=raises) + return check_csrf_token(*args, **kwargs) - def test_success(self): + def test_success_token(self): request = testing.DummyRequest() request.params['csrf_token'] = request.session.get_csrf_token() - self.assertEqual(self._callFUT(request, 'csrf_token'), True) + self.assertEqual(self._callFUT(request, token='csrf_token'), True) + + def test_success_header(self): + request = testing.DummyRequest() + request.headers['X-CSRFToken'] = request.session.get_csrf_token() + self.assertEqual(self._callFUT(request, header='X-CSRFToken'), True) def test_success_default_token(self): - from ..session import check_csrf_token request = testing.DummyRequest() request.params['csrf_token'] = request.session.get_csrf_token() - self.assertEqual(check_csrf_token(request), True) + self.assertEqual(self._callFUT(request), True) + + def test_success_default_header(self): + request = testing.DummyRequest() + request.headers['X-CSRFToken'] = request.session.get_csrf_token() + self.assertEqual(self._callFUT(request), True) def test_failure_raises(self): from pyramid.httpexceptions import HTTPBadRequest -- cgit v1.2.3 From 26b0d13f2973c46cac28c209f7c67b10bfc91b62 Mon Sep 17 00:00:00 2001 From: Luke Cyca Date: Thu, 30 May 2013 10:28:11 -0700 Subject: Added myself to contributors --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 97eb54f7b..61155ca80 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -198,3 +198,5 @@ Contributors - Georges Dubus, 2013/03/21 - Jason McKellar, 2013/03/28 + +- Luke Cyca, 2013/05/30 -- cgit v1.2.3 From ea93cfd8295b215a19fcc0cd0f28ec9810616528 Mon Sep 17 00:00:00 2001 From: Luke Cyca Date: Sun, 2 Jun 2013 19:05:36 -0700 Subject: Changed header name to X-CSRF-Token --- pyramid/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyramid/session.py b/pyramid/session.py index 0433488d8..3708ef879 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -83,14 +83,14 @@ def signed_deserialize(serialized, secret, hmac=hmac): def check_csrf_token(request, token='csrf_token', - header='X-CSRFToken', + header='X-CSRF-Token', raises=True): """ Check the CSRF token in the request's session against the value in ``request.params.get(token)`` or ``request.headers.get(header)``. If a ``token`` keyword is not supplied to this function, the string ``csrf_token`` will be used to look up the token in ``request.params``. If a ``header`` keyword is not supplied to this function, the string - ``X-CSRFToken`` will be used to look up the token in ``request.headers``. + ``X-CSRF-Token`` will be used to look up the token in ``request.headers``. If the value supplied by param or by header doesn't match the value supplied by ``request.session.get_csrf_token()``, and ``raises`` is -- cgit v1.2.3 From d95a2732eb2f972df9fb2f954ae374b8acd06727 Mon Sep 17 00:00:00 2001 From: Luke Cyca Date: Sun, 2 Jun 2013 19:08:44 -0700 Subject: Edited narrative docs about CSRF --- docs/narr/sessions.rst | 53 +++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index c4f4b5f07..52b4860b3 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -298,14 +298,15 @@ Preventing Cross-Site Request Forgery Attacks `Cross-site request forgery `_ attacks are a -phenomenon whereby a user with an identity on your website might click on a -URL or button on another website which secretly redirects the user to your -application to perform some command that requires elevated privileges. - -You can avoid most of these attacks by making sure that the correct *CSRF -token* has been set in an :app:`Pyramid` session object before performing any -actions in code which requires elevated privileges that is invoked via a form -post. To use CSRF token support, you must enable a :term:`session factory` +phenomenon whereby a user who is logged in to your website might inadvertantly +load a URL because it is linked from, or embedded in, an attacker's website. +If the URL is one that may modify or delete data, the consequences can be dire. + +You can avoid most of these attacks by issuing a unique token to the browser +and then requiring that it be present in all potentially unsafe requests. +:app:`Pyramid` sessions provide facilities to create and check CSRF tokens. + +To use CSRF tokens, you must first enable a :term:`session factory` as described in :ref:`using_the_default_session_factory` or :ref:`using_alternate_session_factories`. @@ -324,33 +325,41 @@ To get the current CSRF token from the session, use the The ``session.get_csrf_token()`` method accepts no arguments. It returns a CSRF *token* string. If ``session.get_csrf_token()`` or -``session.new_csrf_token()`` was invoked previously for this session, the +``session.new_csrf_token()`` was invoked previously for this session, then the existing token will be returned. If no CSRF token previously existed for -this session, a new token will be will be set into the session and returned. +this session, then a new token will be will be set into the session and returned. The newly created token will be opaque and randomized. You can use the returned token as the value of a hidden field in a form that -posts to a method that requires elevated privileges. The handler for the -form post should use ``session.get_csrf_token()`` *again* to obtain the -current CSRF token related to the user from the session, and compare it to -the value of the hidden form field. For example, if your form rendering -included the CSRF token obtained via ``session.get_csrf_token()`` as a hidden -input field named ``csrf_token``: +posts to a method that requires elevated privileges, or supply it as a request +header in AJAX requests. The handler for the URL that receives the request +should then require that the correct CSRF token is supplied. -.. code-block:: python - :linenos: +Using the ``session.check_csrf_token`` Method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - token = request.session.get_csrf_token() - if token != request.POST['csrf_token']: - raise ValueError('CSRF token did not match') +In request handling code, you can check the presence and validity of a CSRF +token with ``session.check_csrf_token(request)``. If the token is valid, +it will return True, otherwise it will raise ``HTTPBadRequest``. + +By default, it checks for a GET or POST parameter named ``csrf_token`` or a +header named ``X-CSRF-Token``. .. index:: single: session.new_csrf_token +Checking CSRF Tokens With A View Predicate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A convenient way to require a valid CSRF Token for a particular view is to +include ``check_csrf=True`` as a view predicate. +See :meth:`pyramid.config.Configurator.add_route`. + + Using the ``session.new_csrf_token`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To explicitly add a new CSRF token to the session, use the +To explicitly create a new CSRF token, use the ``session.new_csrf_token()`` method. This differs only from ``session.get_csrf_token()`` inasmuch as it clears any existing CSRF token, creates a new CSRF token, sets the token into the session, and returns the -- cgit v1.2.3 From 009f843d7d72d3a9d8cc35c08db9b77e247111f5 Mon Sep 17 00:00:00 2001 From: Luke Cyca Date: Tue, 4 Jun 2013 22:25:37 -0700 Subject: Add examples to narrative CSRF docs --- docs/narr/sessions.rst | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 52b4860b3..7ec280c8a 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -332,7 +332,32 @@ The newly created token will be opaque and randomized. You can use the returned token as the value of a hidden field in a form that posts to a method that requires elevated privileges, or supply it as a request -header in AJAX requests. The handler for the URL that receives the request +header in AJAX requests. + +For example, include the CSRF token as a hidden field: + +.. code-block:: html + +
+ + +
+ +Or, include it as a header in a jQuery AJAX request: + +.. code-block:: javascript + + var csrfToken = ${request.session.get_csrf_token()}; + $.ajax({ + type: "POST", + url: "/myview", + headers: { 'X-CSRF-Token': csrfToken } + }).done(function() { + alert("Deleted"); + }); + + +The handler for the URL that receives the request should then require that the correct CSRF token is supplied. Using the ``session.check_csrf_token`` Method @@ -345,6 +370,16 @@ it will return True, otherwise it will raise ``HTTPBadRequest``. By default, it checks for a GET or POST parameter named ``csrf_token`` or a header named ``X-CSRF-Token``. +.. code-block:: python + + def myview(request): + session = request.session + + # Require CSRF Token + session.check_csrf_token(request): + + ... + .. index:: single: session.new_csrf_token @@ -355,6 +390,12 @@ A convenient way to require a valid CSRF Token for a particular view is to include ``check_csrf=True`` as a view predicate. See :meth:`pyramid.config.Configurator.add_route`. +.. code-block:: python + + @view_config(request_method='POST', check_csrf=True, ...) + def myview(request): + ... + Using the ``session.new_csrf_token`` Method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.2.3 From af9e4210d529e0f28f9429a1338a662e5dec153c Mon Sep 17 00:00:00 2001 From: Luke Cyca Date: Tue, 4 Jun 2013 22:26:12 -0700 Subject: Update tests to use X-CSRF-Token header --- pyramid/tests/test_session.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index d3bafb26e..35e2b5c27 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -367,8 +367,8 @@ class Test_check_csrf_token(unittest.TestCase): def test_success_header(self): request = testing.DummyRequest() - request.headers['X-CSRFToken'] = request.session.get_csrf_token() - self.assertEqual(self._callFUT(request, header='X-CSRFToken'), True) + request.headers['X-CSRF-Token'] = request.session.get_csrf_token() + self.assertEqual(self._callFUT(request, header='X-CSRF-Token'), True) def test_success_default_token(self): request = testing.DummyRequest() @@ -377,7 +377,7 @@ class Test_check_csrf_token(unittest.TestCase): def test_success_default_header(self): request = testing.DummyRequest() - request.headers['X-CSRFToken'] = request.session.get_csrf_token() + request.headers['X-CSRF-Token'] = request.session.get_csrf_token() self.assertEqual(self._callFUT(request), True) def test_failure_raises(self): -- cgit v1.2.3 From fab8454294b6271c727a0251c78b5f55df5788bf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 5 Jun 2013 06:04:45 -0400 Subject: add changelog note --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index a471addce..6a26879a3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -29,6 +29,11 @@ Features ``initialize_myapp_db etc/development.ini a=1 b=2``. See https://github.com/Pylons/pyramid/pull/911 +- The ``request.session.check_csrf_token()`` method and the ``check_csrf`` view + predicate now take into account the value of the HTTP header named + ``X-CSRF-Token`` (as well as the ``csrf_token`` form parameter, which they + always did). The header is tried when the form parameter does not exist. + Bug Fixes --------- -- cgit v1.2.3