diff options
| author | Chris McDonough <chrism@plope.com> | 2010-12-22 18:27:07 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2010-12-22 18:27:07 -0500 |
| commit | 319793d9b3d127ba2a9245713ef4f01b32918e95 (patch) | |
| tree | 651a3ec0365c68d947938265bd5a9223a8d24d86 | |
| parent | 5801195412d2c809182304d09cc2860c61c6cc93 (diff) | |
| download | pyramid-319793d9b3d127ba2a9245713ef4f01b32918e95.tar.gz pyramid-319793d9b3d127ba2a9245713ef4f01b32918e95.tar.bz2 pyramid-319793d9b3d127ba2a9245713ef4f01b32918e95.zip | |
- Added CSRF token generation, as described in the narrative chapter entitled
"Preventing Cross-Site Request Forgery Attacks".
| -rw-r--r-- | CHANGES.txt | 6 | ||||
| -rw-r--r-- | TODO.txt | 4 | ||||
| -rw-r--r-- | docs/api.rst | 1 | ||||
| -rw-r--r-- | docs/api/flash.rst | 36 | ||||
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rw-r--r-- | docs/latexindex.rst | 2 | ||||
| -rw-r--r-- | docs/narr/csrf.rst | 63 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 13 | ||||
| -rw-r--r-- | pyramid/session.py | 17 | ||||
| -rw-r--r-- | pyramid/tests/test_session.py | 14 |
10 files changed, 112 insertions, 45 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 9fb26b589..775e24aec 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,6 +12,9 @@ Features - Added flash messaging, as described in the "Flash Messaging" narrative documentation chapter. +- Added CSRF token generation, as described in the narrative chapter entitled + "Preventing Cross-Site Request Forgery Attacks". + Documentation ------------- @@ -38,6 +41,9 @@ Documentation - Added a "Flash Messaging" narrative documentation chapter. +- Added a narrative chapter entitled "Preventing Cross-Site Request Forgery + Attacks". + 1.0a7 (2010-12-20) ================== @@ -18,10 +18,6 @@ Should-Have - translationdir ZCML directive use of ``path_spec`` should maybe die. -- Add CRSF token creation/checking machinery (only "should have" vs. "must - have" because I'm not sure it belongs in Pyramid.. it definitely must exist - in formgen libraries, and *might* belong in Pyramid). - - Change "Cleaning up After a Request" in the urldispatch chapter to use ``request.add_response_callback``. diff --git a/docs/api.rst b/docs/api.rst index 4808a08b3..b650c8ded 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -15,7 +15,6 @@ documentation is organized alphabetically by module name. api/config api/events api/exceptions - api/flash api/httpexceptions api/i18n api/interfaces diff --git a/docs/api/flash.rst b/docs/api/flash.rst deleted file mode 100644 index 94907958d..000000000 --- a/docs/api/flash.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. _flash_module: - -:mod:`pyramid.flash` --------------------- - -Flash Category Constants -~~~~~~~~~~~~~~~~~~~~~~~~ - -The following attributes represent constants for use as flash messaging -category values (see :ref:`flash_chapter`). - -.. attribute:: DEBUG - - An alternate spelling for the string ``debug``. Represents development - debug messages. - -.. attribute:: INFO - - An alternate spelling for the string ``info``. Represents messages that - are informational for user consumption. - -.. attribute:: SUCCESS - - An alternate spelling for the string ``success``. Represents messages that - tell the user about a successful action. - -.. attribute:: WARNING - - An alternate spelling for the string ``warning``. Represents messages - that tell the user about a condition that is not a success, but is neither - an error. - -.. attribute:: ERROR - - An alternate spelling for the string ``success``. Represents messages - that tell the user about an unsuccessful action. diff --git a/docs/index.rst b/docs/index.rst index fbf9de810..343fb28ba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,6 +49,7 @@ Narrative documentation in chapter form explaining how to use narr/webob narr/sessions narr/flash + narr/csrf narr/security narr/hybrid narr/i18n diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 6a1992ba4..713c9841f 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -42,6 +42,7 @@ Narrative Documentation narr/webob narr/sessions narr/flash + narr/csrf narr/security narr/hybrid narr/i18n @@ -89,7 +90,6 @@ API Reference api/config api/events api/exceptions - api/flash api/httpexceptions api/i18n api/interfaces diff --git a/docs/narr/csrf.rst b/docs/narr/csrf.rst new file mode 100644 index 000000000..7d1ee6fea --- /dev/null +++ b/docs/narr/csrf.rst @@ -0,0 +1,63 @@ +.. _csrf_chapter: + +Preventing Cross-Site Request Forgery Attacks +============================================= + +`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 unwittingly 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 a the correct *CSRF +token* has been set in an :app:`Pyramid` session object before performing any +actions in code which requires elevated privileges and is invoked via a form +post. To use CSRF token support, you must enable a :term:`session factory` +as described in :ref:`using_the_default_session_factory` or +:ref:`using_alternate_session_factories`. + +Using the ``session.new_csrf_token`` Method +------------------------------------------- + +To add a CSRF token to the session, use the ``session.new_csrf_token`` method. + +.. code-block:: python + :linenos: + + token = request.session.new_csrf_token() + +The ``.new_csrf_token`` method accepts no arguments. It returns a *token* +string, which will be opaque and randomized. This token will also be set +into the session, awaiting pickup by the ``session.pop_csrf_token`` method. +You can subsequently 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.pop_csrf_token`` (explained below) to +pop the current CSRF token related to the user from the session, and compare +it to the value of the hidden form field. + +Using the ``session.pop_csrf_token`` Method +------------------------------------------- + +To pop the current CSRF token from the session, use the +``session.pop_csrf_token`` method. + +.. code-block:: python + :linenos: + + token = request.session.pop_csrf_token() + +The ``.pop_csrf_token`` method accepts no arguments. It returns the +"current" *token* string (as per the last call to +``session.new_csrf_token``). You can then use it to compare against the +token provided within form post hidden value data. For example, if your form +rendering included the CSRF token obtained via ``session.new_csrf_token`` as +a hidden input field named ``csrf_token``: + +.. code-block:: python + :linenos: + + token = request.session.pop_csrf_token() + if token != request.POST['csrf_token']: + raise ValueError('CSRF token did not match') + + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 45a63dc8b..aa537d633 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -477,7 +477,18 @@ class ISession(Interface): """ Peek at a queue in the flash storage. The queue remains in flash storage after this message is called. The queue is returned; it is a list of flash messages added by - :meth:`pyramid.interfaces.ISesssion.flash`""" + :meth:`pyramid.interfaces.ISesssion.flash` + """ + + def new_csrf_token(self): + """ Create and set into the session a new, random cross-site request + forgery protection token. Return the token. It will be a string.""" + + def pop_csrf_token(self): + """ Pop any CSRF token previously added to the session via + ``new_csrf_token``, and return the token. If no CSRF token exists, + the value returned will be ``None``. + """ # mapping methods diff --git a/pyramid/session.py b/pyramid/session.py index 97db09e98..b138af7c7 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -10,10 +10,11 @@ except ImportError: # pragma: no cover from webob import Response -import hmac +import base64 import binascii +import hmac import time -import base64 +import os from zope.interface import implements @@ -179,10 +180,22 @@ def UnencryptedCookieSessionFactoryConfig( storage = self.pop('_f_' + queue, []) return storage + @manage_accessed def peek_flash(self, queue=''): storage = self.get('_f_' + queue, []) return storage + # CSRF API methods + @manage_accessed + def new_csrf_token(self): + token = os.urandom(20).encode('hex') + self['_csrft_'] = token + return token + + @manage_accessed + def pop_csrf_token(self): + return self.pop('_csrft_', None) + # non-API methods def _set_cookie(self, response): if not self._cookie_on_exception: diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 930e4697b..449ef02a5 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -164,6 +164,20 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertEqual(result, queue) self.assertEqual(session.get('_f_error'), queue) + def test_new_csrf_token(self): + request = testing.DummyRequest() + session = self._makeOne(request) + token = session.new_csrf_token() + self.assertEqual(token, session['_csrft_']) + + def test_pop_csrf_token(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session['_csrft_'] = 'token' + token = session.pop_csrf_token() + self.assertEqual(token, 'token') + self.failIf('_csrft_' in session) + class Test_manage_accessed(unittest.TestCase): def _makeOne(self, wrapped): from pyramid.session import manage_accessed |
