Skip to content

Commit

Permalink
rewrite csrf checks to support a global setting to turn it on
Browse files Browse the repository at this point in the history
- only check csrf on POST
- support "pyramid.require_default_csrf" setting
- support "require_csrf=True" to fallback to the global setting to
  determine the token name
  • Loading branch information
mmerickel committed Apr 11, 2016
1 parent 9e9fa9a commit 6b35eb6
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 87 deletions.
8 changes: 8 additions & 0 deletions docs/glossary.rst
Expand Up @@ -1098,3 +1098,11 @@ Glossary
implementing the :class:`pyramid.interfaces.IViewDeriver` interface.
Examples of built-in derivers including view mapper, the permission
checker, and applying a renderer to a dictionary returned from the view.

truthy string
A string represeting a value of ``True``. Acceptable values are
``t``, ``true``, ``y``, ``yes``, ``on`` and ``1``.

falsey string
A string represeting a value of ``False``. Acceptable values are
``f``, ``false``, ``n``, ``no``, ``off`` and ``0``.
52 changes: 6 additions & 46 deletions docs/narr/hooks.rst
Expand Up @@ -1580,11 +1580,6 @@ There are several built-in view derivers that :app:`Pyramid` will automatically
apply to any view. Below they are defined in order from furthest to closest to
the user-defined :term:`view callable`:

``csrf_view``

Used to check the CSRF token provided in the request. This element is a
no-op if ``require_csrf`` is not defined.

``secured_view``

Enforce the ``permission`` defined on the view. This element is a no-op if no
Expand All @@ -1595,6 +1590,12 @@ the user-defined :term:`view callable`:
This element will also output useful debugging information when
``pyramid.debug_authorization`` is enabled.

``csrf_view``

Used to check the CSRF token provided in the request. This element is a
no-op if both the ``require_csrf`` view option and the
``pyramid.require_default_csrf`` setting are disabled.

``owrapped_view``

Invokes the wrapped view defined by the ``wrapper`` option.
Expand Down Expand Up @@ -1661,47 +1662,6 @@ View derivers are unique in that they have access to most of the options
passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what
to do, and they have a chance to affect every view in the application.

Let's override the default CSRF checker to default to on instead of off and
only check ``POST`` requests:

.. code-block:: python
:linenos:
from pyramid.response import Response
from pyramid.session import check_csrf_token
from pyramid.viewderivers import INGRESS
def csrf_view(view, info):
val = info.options.get('require_csrf', True)
wrapper_view = view
if val:
if val is True:
val = 'csrf_token'
def csrf_view(context, request):
if request.method == 'POST':
check_csrf_token(request, val, raises=True)
return view(context, request)
wrapper_view = csrf_view
return wrapper_view
csrf_view.options = ('require_csrf',)
config.add_view_deriver(csrf_view, 'csrf_view', over='secured_view', under=INGRESS)
def protected_view(request):
return Response('protected')
def unprotected_view(request):
return Response('unprotected')
config.add_view(protected_view, name='safe')
config.add_view(unprotected_view, name='unsafe', require_csrf=False)
Navigating to ``/safe`` with a POST request will then fail when the call to
:func:`pyramid.session.check_csrf_token` raises a
:class:`pyramid.exceptions.BadCSRFToken` exception. However, ``/unsafe`` will
not error.

Ordering View Derivers
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
42 changes: 40 additions & 2 deletions docs/narr/sessions.rst
Expand Up @@ -389,8 +389,43 @@ header named ``X-CSRF-Token``.
# ...
.. index::
single: session.new_csrf_token
.. _auto_csrf_checking:

Checking CSRF Tokens Automatically
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 1.7

:app:`Pyramid` supports automatically checking CSRF tokens on POST requests.
Any other request may be checked manually. This feature can be turned on
globally for an application using the ``pyramid.require_default_csrf`` setting.

If the ``pyramid.required_default_csrf`` setting is a :term:`truthy string` or
``True`` then the default CSRF token parameter will be ``csrf_token``. If a
different token is desired, it may be passed as the value. Finally, a
:term:`falsey string` or ``False`` will turn off automatic CSRF checking
globally on every POST request.

No matter what, CSRF checking may be explicitly enabled or disabled on a
per-view basis using the ``require_csrf`` view option. This option is of the
same format as the ``pyramid.require_default_csrf`` setting, accepting strings
or boolean values.

If ``require_csrf`` is ``True`` but does not explicitly define a token to
check, then the token name is pulled from whatever was set in the
``pyramid.require_default_csrf`` setting. Finally, if that setting does not
explicitly define a token, then ``csrf_token`` is the token required. This token
name will be required in ``request.params`` which is a combination of the
query string and a submitted form body.

It is always possible to pass the token in the ``X-CSRF-Token`` header as well.
There is currently no way to define an alternate name for this header without
performing CSRF checking manually.

If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` exception
will be raised. This exception may be caught and handled by an
:term:`exception view` but, by default, will result in a ``400 Bad Request``
resposne being sent to the client.

Checking CSRF Tokens with a View Predicate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -411,6 +446,9 @@ include ``check_csrf=True`` as a view predicate. See
instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different
from calling :func:`pyramid.session.check_csrf_token`.

.. index::
single: session.new_csrf_token

Using the ``session.new_csrf_token`` Method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
7 changes: 6 additions & 1 deletion pyramid/config/settings.py
Expand Up @@ -6,6 +6,7 @@
from pyramid.interfaces import ISettings

from pyramid.settings import asbool
from pyramid.settings import truthy

class SettingsConfiguratorMixin(object):
def _set_settings(self, mapping):
Expand Down Expand Up @@ -122,6 +123,8 @@ def __init__(self, d=None, _environ_=os.environ, **kw):
config_prevent_cachebust)
eff_prevent_cachebust = asbool(eget('PYRAMID_PREVENT_CACHEBUST',
config_prevent_cachebust))
require_default_csrf = self.get('pyramid.require_default_csrf')
eff_require_default_csrf = require_default_csrf

update = {
'debug_authorization': eff_debug_all or eff_debug_auth,
Expand All @@ -134,6 +137,7 @@ def __init__(self, d=None, _environ_=os.environ, **kw):
'default_locale_name':eff_locale_name,
'prevent_http_cache':eff_prevent_http_cache,
'prevent_cachebust':eff_prevent_cachebust,
'require_default_csrf':eff_require_default_csrf,

'pyramid.debug_authorization': eff_debug_all or eff_debug_auth,
'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound,
Expand All @@ -145,7 +149,8 @@ def __init__(self, d=None, _environ_=os.environ, **kw):
'pyramid.default_locale_name':eff_locale_name,
'pyramid.prevent_http_cache':eff_prevent_http_cache,
'pyramid.prevent_cachebust':eff_prevent_cachebust,
}
'pyramid.require_default_csrf':eff_require_default_csrf,
}

self.update(update)

Expand Down
37 changes: 18 additions & 19 deletions pyramid/config/views.py
Expand Up @@ -371,27 +371,26 @@ def add_view(
.. versionadded:: 1.7
If specified, this value should be one of ``None``, ``True``,
``False``, or a string representing the 'check name'. If the value
is ``True`` or a string, CSRF checking will be performed. If the
value is ``False`` or ``None``, CSRF checking will not be performed.
CSRF checks only affect POST requests. Any other request methods
will pass untouched. This option is used in combination with the
``pyramid.require_default_csrf`` setting to control which
request parameters are checked for CSRF tokens.
If the value provided is a string, that string will be used as the
'check name'. If the value provided is ``True``, ``csrf_token`` will
be used as the check name.
This feature requires a configured :term:`session factory`.
If CSRF checking is performed, the checked value will be the value
of ``request.params[check_name]``. This value will be compared
against the value of ``request.session.get_csrf_token()``, and the
check will pass if these two values are the same. If the check
passes, the associated view will be permitted to execute. If the
check fails, the associated view will not be permitted to execute
and a :class:`pyramid.exceptions.BadCSRFToken` exception will
be raised. This exception may be caught and handled by an
:term:`exception view`.
If this option is set to ``True`` then CSRF checks will be enabled
for POST requests to this view. The required token will be whatever
was specified by the ``pyramid.require_default_csrf`` setting, or
will fallback to ``csrf_token``.
Note that using this feature requires a :term:`session factory` to
have been configured.
If this option is set to a string then CSRF checks will be enabled
and it will be used as the required token regardless of the
``pyramid.require_default_csrf`` setting.
If this option is set to ``False`` then CSRF checks will be disabled
regardless of the ``pyramid.require_default_csrf`` setting.
See :ref:`auto_csrf_checking` for more information.
wrapper
Expand Down Expand Up @@ -1213,8 +1212,8 @@ def register():
def add_default_view_derivers(self):
d = pyramid.viewderivers
derivers = [
('csrf_view', d.csrf_view),
('secured_view', d.secured_view),
('csrf_view', d.csrf_view),
('owrapped_view', d.owrapped_view),
('http_cached_view', d.http_cached_view),
('decorated_view', d.decorated_view),
Expand Down
7 changes: 3 additions & 4 deletions pyramid/settings.py
@@ -1,13 +1,12 @@
from pyramid.compat import string_types

truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
falsey = frozenset(('f', 'false', 'n', 'no', 'off', '0'))

def asbool(s):
""" Return the boolean value ``True`` if the case-lowered value of string
input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise
return the boolean value ``False``. If ``s`` is the value ``None``,
return ``False``. If ``s`` is already one of the boolean values ``True``
or ``False``, return it."""
input ``s`` is a :term:`truthy string`. If ``s`` is already one of the
boolean values ``True`` or ``False``, return it."""
if s is None:
return False
if isinstance(s, bool):
Expand Down
17 changes: 10 additions & 7 deletions pyramid/tests/test_config/test_views.py
Expand Up @@ -1570,28 +1570,30 @@ def test_add_view_same_predicates(self):
config.add_view(view=view2)
self.assertRaises(ConfigurationConflictError, config.commit)

def test_add_view_with_csrf_header(self):
def test_add_view_with_csrf_param(self):
from pyramid.renderers import null_renderer
def view(request):
return 'OK'
config = self._makeOne(autocommit=True)
config.add_view(view, require_csrf=True, renderer=null_renderer)
config.add_view(view, require_csrf='st', renderer=null_renderer)
view = self._getViewCallable(config)
request = self._makeRequest(config)
request.headers = {'X-CSRF-Token': 'foo'}
request.method = 'POST'
request.params = {'st': 'foo'}
request.headers = {}
request.session = DummySession({'csrf_token': 'foo'})
self.assertEqual(view(None, request), 'OK')

def test_add_view_with_csrf_param(self):
def test_add_view_with_csrf_header(self):
from pyramid.renderers import null_renderer
def view(request):
return 'OK'
config = self._makeOne(autocommit=True)
config.add_view(view, require_csrf='st', renderer=null_renderer)
config.add_view(view, require_csrf=True, renderer=null_renderer)
view = self._getViewCallable(config)
request = self._makeRequest(config)
request.params = {'st': 'foo'}
request.headers = {}
request.method = 'POST'
request.headers = {'X-CSRF-Token': 'foo'}
request.session = DummySession({'csrf_token': 'foo'})
self.assertEqual(view(None, request), 'OK')

Expand All @@ -1603,6 +1605,7 @@ def view(request): return 'OK'
config.add_view(view, require_csrf=True, renderer=null_renderer)
view = self._getViewCallable(config)
request = self._makeRequest(config)
request.method = 'POST'
request.headers = {}
request.session = DummySession({'csrf_token': 'foo'})
self.assertRaises(BadCSRFToken, lambda: view(None, request))
Expand Down

0 comments on commit 6b35eb6

Please sign in to comment.