Skip to content

Commit

Permalink
Merge pull request Pylons#2413 from mmerickel/feature/require-csrf
Browse files Browse the repository at this point in the history
require_csrf to replace check_csrf
  • Loading branch information
digitalresistor committed Apr 13, 2016
2 parents b1527e7 + 231a531 commit d26e3af
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 58 deletions.
8 changes: 8 additions & 0 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,14 @@ Glossary
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``.

pip
The `Python Packaging Authority's <https://www.pypa.io/>`_ recommended
tool for installing Python packages.
Expand Down
42 changes: 6 additions & 36 deletions docs/narr/hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1590,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 @@ -1656,42 +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 look at one more example which will protect views by requiring a CSRF
token unless ``disable_csrf=True`` is passed to the view:

.. code-block:: python
:linenos:
from pyramid.response import Response
from pyramid.session import check_csrf_token
def require_csrf_view(view, info):
wrapper_view = view
if not info.options.get('disable_csrf', False):
def wrapper_view(context, request):
if request.method == 'POST':
check_csrf_token(request)
return view(context, request)
return wrapper_view
require_csrf_view.options = ('disable_csrf',)
config.add_view_deriver(require_csrf_view)
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', disable_csrf=True)
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
70 changes: 56 additions & 14 deletions docs/narr/sessions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,21 @@ Or include it as a header in a jQuery AJAX request:
The handler for the URL that receives the request should then require that the
correct CSRF token is supplied.

.. index::
single: session.new_csrf_token

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

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 token.

.. code-block:: python
token = request.session.new_csrf_token()
Checking CSRF Tokens Manually
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -389,12 +404,51 @@ 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``
response being sent to the client.

Checking CSRF Tokens with a View Predicate
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. deprecated:: 1.7
Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead
to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised.

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_view`.
Expand All @@ -410,15 +464,3 @@ include ``check_csrf=True`` as a view predicate. See
predicate system, when it doesn't find a view, raises ``HTTPNotFound``
instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different
from calling :func:`pyramid.session.check_csrf_token`.

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

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 token.

.. code-block:: python
token = request.session.new_csrf_token()
26 changes: 26 additions & 0 deletions docs/narr/viewconfig.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,32 @@ Non-Predicate Arguments
only influence ``Cache-Control`` headers, pass a tuple as ``http_cache`` with
the first element of ``None``, i.e., ``(None, {'public':True})``.


``require_csrf``

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.

This feature requires a configured :term:`session factory`.

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``.

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.

.. versionadded:: 1.7

``wrapper``
The :term:`view name` of a different :term:`view configuration` which will
receive the response body of this view as the ``request.wrapped_body``
Expand Down
6 changes: 5 additions & 1 deletion pyramid/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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 +136,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 +148,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
51 changes: 49 additions & 2 deletions pyramid/config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ def add_view(
http_cache=None,
match_param=None,
check_csrf=None,
require_csrf=None,
**view_options):
""" Add a :term:`view configuration` to the current
configuration state. Arguments to ``add_view`` are broken
Expand Down Expand Up @@ -366,6 +367,31 @@ def add_view(
before returning the response from the view. This effectively
disables any HTTP caching done by ``http_cache`` for that response.
require_csrf
.. versionadded:: 1.7
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.
This feature requires a configured :term:`session factory`.
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``.
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
The :term:`view name` of a different :term:`view
Expand Down Expand Up @@ -587,6 +613,11 @@ def wrapper(context, request):
check_csrf
.. deprecated:: 1.7
Use the ``require_csrf`` option or see :ref:`auto_csrf_checking`
instead to have :class:`pyramid.exceptions.BadCSRFToken`
exceptions raised.
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
Expand Down Expand Up @@ -682,7 +713,18 @@ def wrapper(context, request):
'Predicate" in the "Hooks" chapter of the documentation '
'for more information.'),
DeprecationWarning,
stacklevel=4
stacklevel=4,
)

if check_csrf is not None:
warnings.warn(
('The "check_csrf" argument to Configurator.add_view is '
'deprecated as of Pyramid 1.7. Use the "require_csrf" option '
'instead or see "Checking CSRF Tokens Automatically" in the '
'"Sessions" chapter of the documentation for more '
'information.'),
DeprecationWarning,
stacklevel=4,
)

view = self.maybe_dotted(view)
Expand Down Expand Up @@ -805,6 +847,8 @@ def discrim_func():
path_info=path_info,
match_param=match_param,
check_csrf=check_csrf,
http_cache=http_cache,
require_csrf=require_csrf,
callable=view,
mapper=mapper,
decorator=decorator,
Expand Down Expand Up @@ -860,6 +904,7 @@ def register(permission=permission, renderer=renderer):
decorator=decorator,
mapper=mapper,
http_cache=http_cache,
require_csrf=require_csrf,
extra_options=ovals,
)
derived_view.__discriminator__ = lambda *arg: discriminator
Expand Down Expand Up @@ -1184,6 +1229,7 @@ def add_default_view_derivers(self):
d = pyramid.viewderivers
derivers = [
('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 Expand Up @@ -1284,7 +1330,7 @@ def _derive_view(self, view, permission=None, predicates=(),
viewname=None, accept=None, order=MAX_ORDER,
phash=DEFAULT_PHASH, decorator=None,
mapper=None, http_cache=None, context=None,
extra_options=None):
require_csrf=None, extra_options=None):
view = self.maybe_dotted(view)
mapper = self.maybe_dotted(mapper)
if isinstance(renderer, string_types):
Expand All @@ -1311,6 +1357,7 @@ def _derive_view(self, view, permission=None, predicates=(),
mapper=mapper,
decorator=decorator,
http_cache=http_cache,
require_csrf=require_csrf,
)
if extra_options:
options.update(extra_options)
Expand Down
3 changes: 3 additions & 0 deletions pyramid/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ def check_csrf_token(request,
Note that using this function requires that a :term:`session factory` is
configured.
See :ref:`auto_csrf_checking` for information about how to secure your
application automatically against CSRF attacks.
.. versionadded:: 1.4a2
"""
supplied_token = request.params.get(token, request.headers.get(header, ""))
Expand Down
7 changes: 3 additions & 4 deletions pyramid/settings.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit d26e3af

Please sign in to comment.