summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2010-12-22 18:27:07 -0500
committerChris McDonough <chrism@plope.com>2010-12-22 18:27:07 -0500
commit319793d9b3d127ba2a9245713ef4f01b32918e95 (patch)
tree651a3ec0365c68d947938265bd5a9223a8d24d86
parent5801195412d2c809182304d09cc2860c61c6cc93 (diff)
downloadpyramid-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.txt6
-rw-r--r--TODO.txt4
-rw-r--r--docs/api.rst1
-rw-r--r--docs/api/flash.rst36
-rw-r--r--docs/index.rst1
-rw-r--r--docs/latexindex.rst2
-rw-r--r--docs/narr/csrf.rst63
-rw-r--r--pyramid/interfaces.py13
-rw-r--r--pyramid/session.py17
-rw-r--r--pyramid/tests/test_session.py14
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)
==================
diff --git a/TODO.txt b/TODO.txt
index 0e8a935da..3a32322f8 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -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