Chris McDonough
2013-06-05 806063db53955a94ff52aa42fe38bc30a851c166
Merge branch 'lukecyca-master'
5 files modified
144 ■■■■ changed files
CHANGES.txt 5 ●●●●● patch | view | raw | blame | history
CONTRIBUTORS.txt 2 ●●●●● patch | view | raw | blame | history
docs/narr/sessions.rst 90 ●●●● patch | view | raw | blame | history
pyramid/session.py 26 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_session.py 21 ●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -29,6 +29,11 @@
  ``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
---------
CONTRIBUTORS.txt
@@ -198,3 +198,5 @@
- Georges Dubus, 2013/03/21
- Jason McKellar, 2013/03/28
- Luke Cyca, 2013/05/30
docs/narr/sessions.rst
@@ -298,14 +298,15 @@
`Cross-site request forgery
<http://en.wikipedia.org/wiki/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.
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 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`
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,82 @@
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.
For example, include the CSRF token as a hidden field:
.. code-block:: html
    <form method="post" action="/myview">
      <input type="hidden" name="csrf_token" value="${request.session.get_csrf_token()}">
      <input type="submit" value="Delete Everything">
    </form>
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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``.
.. code-block:: python
   :linenos:
   token = request.session.get_csrf_token()
   if token != request.POST['csrf_token']:
       raise ValueError('CSRF token did not match')
    def myview(request):
        session = request.session
        # Require CSRF Token
        session.check_csrf_token(request):
        ...
.. 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`.
.. code-block:: python
    @view_config(request_method='POST', check_csrf=True, ...)
    def myview(request):
        ...
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
pyramid/session.py
@@ -81,15 +81,22 @@
    return pickle.loads(pickled)
def check_csrf_token(request, token='csrf_token', raises=True):
def check_csrf_token(request,
                     token='csrf_token',
                     header='X-CSRF-Token',
                     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-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
    ``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 @@
    .. 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
pyramid/tests/test_session.py
@@ -356,20 +356,29 @@
        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-CSRF-Token'] = request.session.get_csrf_token()
        self.assertEqual(self._callFUT(request, header='X-CSRF-Token'), 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-CSRF-Token'] = request.session.get_csrf_token()
        self.assertEqual(self._callFUT(request), True)
    def test_failure_raises(self):
        from pyramid.httpexceptions import HTTPBadRequest