Skip to content

Commit

Permalink
Merge pull request Pylons#2518 from mmerickel/feature/set-default-csr…
Browse files Browse the repository at this point in the history
…f-options

replace pyramid.require_default_csrf setting with config.set_default_csrf_options
  • Loading branch information
mmerickel committed Apr 20, 2016
2 parents 6c16fb0 + de3d0c7 commit fe463dd
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 180 deletions.
2 changes: 2 additions & 0 deletions docs/api/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

.. automethod:: set_authentication_policy
.. automethod:: set_authorization_policy
.. automethod:: set_default_csrf_options
.. automethod:: set_default_permission
.. automethod:: add_permission

Expand Down Expand Up @@ -65,6 +66,7 @@
.. automethod:: add_traverser
.. automethod:: add_tween
.. automethod:: add_route_predicate
.. automethod:: add_subscriber_predicate
.. automethod:: add_view_predicate
.. automethod:: add_view_deriver
.. automethod:: set_request_factory
Expand Down
1 change: 1 addition & 0 deletions docs/narr/extconfig.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ Pre-defined Phases
- :meth:`pyramid.config.Configurator.add_view_predicate`
- :meth:`pyramid.config.Configurator.add_view_deriver`
- :meth:`pyramid.config.Configurator.set_authorization_policy`
- :meth:`pyramid.config.Configurator.set_default_csrf_options`
- :meth:`pyramid.config.Configurator.set_default_permission`
- :meth:`pyramid.config.Configurator.set_view_mapper`

Expand Down
25 changes: 25 additions & 0 deletions docs/narr/introspector.rst
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,31 @@ introspectables in categories not described here.

The permission name passed to ``set_default_permission``.

``default csrf options``

There will be one and only one introspectable in the ``default csrf options``
category. It represents a call to the
:meth:`pyramid.config.Configurator.set_default_csrf_options` method. It
will have the following data.

``require_csrf``

The default value for ``require_csrf`` if left unspecified on calls to
:meth:`pyramid.config.Configurator.add_view`.

``token``

The name of the token searched in ``request.POST`` to find a valid CSRF
token.

``header``

The name of the request header searched to find a valid CSRF token.

``safe_methods``

The list of HTTP methods considered safe and exempt from CSRF checks.

``views``

Each introspectable in the ``views`` category represents a call to
Expand Down
76 changes: 40 additions & 36 deletions docs/narr/sessions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -396,13 +396,13 @@ named ``X-CSRF-Token``.

.. code-block:: python
from pyramid.session import check_csrf_token
from pyramid.session import check_csrf_token
def myview(request):
# Require CSRF Token
check_csrf_token(request)
def myview(request):
# Require CSRF Token
check_csrf_token(request)
# ...
# ...
.. _auto_csrf_checking:

Expand All @@ -414,41 +414,45 @@ Checking CSRF Tokens Automatically
:app:`Pyramid` supports automatically checking CSRF tokens on requests with an
unsafe method as defined by RFC2616. 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 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.POST`` which is the 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.

In addition to token based CSRF checks, the automatic CSRF checking will also
check the referrer of the request to ensure that it matches one of the trusted
origins. By default the only trusted origin is the current host, however
additional origins may be configured by setting
:meth:`pyramid.config.Configurator.set_default_csrf_options` directive.
For example:

.. code-block:: python
from pyramid.config import Configurator
config = Configurator()
config.set_default_csrf_options(require_csrf=True)
CSRF checking may be explicitly enabled or disabled on a per-view basis using
the ``require_csrf`` view option. A value of ``True`` or ``False`` will
override the default set by ``set_default_csrf_options``. For example:

.. code-block:: python
@view_config(route_name='hello', require_csrf=False)
def myview(request):
# ...
When CSRF checking is active, the token and header used to find the
supplied CSRF token will be ``csrf_token`` and ``X-CSRF-Token``, respectively,
unless otherwise overridden by ``set_default_csrf_options``. The token is
checked against the value in ``request.POST`` which is the submitted form body.
If this value is not present, then the header will be checked.

In addition to token based CSRF checks, if the request is using HTTPS then the
automatic CSRF checking will also check the referrer of the request to ensure
that it matches one of the trusted origins. By default the only trusted origin
is the current host, however additional origins may be configured by setting
``pyramid.csrf_trusted_origins`` to a list of domain names (and ports if they
are non standard). If a host in the list of domains starts with a ``.`` then
that will allow all subdomains as well as the domain without the ``.``.

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.
If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` or
:class:`pyramid.exceptions.BadCSRFOrigin` 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
57 changes: 56 additions & 1 deletion pyramid/config/security.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from zope.interface import implementer

from pyramid.interfaces import (
IAuthorizationPolicy,
IAuthenticationPolicy,
IDefaultCSRFOptions,
IDefaultPermission,
PHASE1_CONFIG,
PHASE2_CONFIG,
)

from pyramid.config.util import as_sorted_tuple
from pyramid.exceptions import ConfigurationError
from pyramid.util import action_method

Expand Down Expand Up @@ -138,7 +142,6 @@ def register():
self.action(IDefaultPermission, register, order=PHASE1_CONFIG,
introspectables=(intr, perm_intr,))


def add_permission(self, permission_name):
"""
A configurator directive which registers a free-standing
Expand All @@ -159,3 +162,55 @@ def add_permission(self, permission_name):
intr['value'] = permission_name
self.action(None, introspectables=(intr,))

@action_method
def set_default_csrf_options(
self,
require_csrf=True,
token='csrf_token',
header='X-CSRF-Token',
safe_methods=('GET', 'HEAD', 'OPTIONS', 'TRACE'),
):
"""
Set the default CSRF options used by subsequent view registrations.
``require_csrf`` controls whether CSRF checks will be automatically
enabled on each view in the application. This value is used as the
fallback when ``require_csrf`` is left at the default of ``None`` on
:meth:`pyramid.config.Configurator.add_view`.
``token`` is the name of the CSRF token used in the body of the
request, accessed via ``request.POST[token]``. Default: ``csrf_token``.
``header`` is the name of the header containing the CSRF token,
accessed via ``request.headers[header]``. Default: ``X-CSRF-Token``.
If ``token`` or ``header`` are set to ``None`` they will not be used
for checking CSRF tokens.
``safe_methods`` is an iterable of HTTP methods which are expected to
not contain side-effects as defined by RFC2616. Safe methods will
never be automatically checked for CSRF tokens.
Default: ``('GET', 'HEAD', 'OPTIONS', TRACE')``.
"""
options = DefaultCSRFOptions(require_csrf, token, header, safe_methods)
def register():
self.registry.registerUtility(options, IDefaultCSRFOptions)
intr = self.introspectable('default csrf view options',
None,
options,
'default csrf view options')
intr['require_csrf'] = require_csrf
intr['token'] = token
intr['header'] = header
intr['safe_methods'] = as_sorted_tuple(safe_methods)
self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG,
introspectables=(intr,))

@implementer(IDefaultCSRFOptions)
class DefaultCSRFOptions(object):
def __init__(self, require_csrf, token, header, safe_methods):
self.require_csrf = require_csrf
self.token = token
self.header = header
self.safe_methods = frozenset(safe_methods)
37 changes: 23 additions & 14 deletions pyramid/config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,24 +371,24 @@ def add_view(
.. 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`.
A boolean option or ``None``. Default: ``None``.
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``.
for requests to this view. The required token or header default to
``csrf_token`` and ``X-CSRF-Token``, respectively.
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.
CSRF checks only affect "unsafe" methods as defined by RFC2616. By
default, these methods are anything except
``GET``, ``HEAD``, ``OPTIONS``, and ``TRACE``.
The defaults here may be overridden by
:meth:`pyramid.config.Configurator.set_default_csrf_options`.
This feature requires a configured :term:`session factory`.
If this option is set to ``False`` then CSRF checks will be disabled
regardless of the ``pyramid.require_default_csrf`` setting.
regardless of the default ``require_csrf`` setting passed
to ``set_default_csrf_options``.
See :ref:`auto_csrf_checking` for more information.
Expand Down Expand Up @@ -1229,7 +1229,6 @@ 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 All @@ -1246,6 +1245,16 @@ def add_default_view_derivers(self):
)
last = name

# leave the csrf_view loosely coupled to the rest of the pipeline
# by ensuring nothing in the default pipeline depends on the order
# of the csrf_view
self.add_view_deriver(
d.csrf_view,
'csrf_view',
under='secured_view',
over='owrapped_view',
)

def derive_view(self, view, attr=None, renderer=None):
"""
Create a :term:`view callable` using the function, instance,
Expand Down
10 changes: 10 additions & 0 deletions pyramid/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,16 @@ class IDefaultPermission(Interface):
for all view configurations which do not explicitly declare their
own."""

class IDefaultCSRFOptions(Interface):
""" An object representing the default CSRF settings to be used for
all view configurations which do not explicitly declare their own."""
require_csrf = Attribute(
'Boolean attribute. If ``True``, then CSRF checks will be enabled by '
'default for the view unless overridden.')
token = Attribute('The key to be matched in the body of the request.')
header = Attribute('The header to be matched with the CSRF token.')
safe_methods = Attribute('A set of safe methods that skip CSRF checks.')

class ISessionFactory(Interface):
""" An interface representing a factory which accepts a request object and
returns an ISession object """
Expand Down
7 changes: 4 additions & 3 deletions pyramid/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ def signed_deserialize(serialized, secret, hmac=hmac):

return pickle.loads(pickled)


def check_csrf_origin(request, trusted_origins=None, raises=True):
"""
Check the Origin of the request to see if it is a cross site request or
Expand Down Expand Up @@ -233,16 +232,18 @@ def check_csrf_token(request,
considered valid. It must be passed in either the request body or
a header.
"""
supplied_token = ""
# If this is a POST/PUT/etc request, then we'll check the body to see if it
# has a token. We explicitly use request.POST here because CSRF tokens
# should never appear in an URL as doing so is a security issue. We also
# explicitly check for request.POST here as we do not support sending form
# encoded data over anything but a request.POST.
supplied_token = request.POST.get(token, "")
if token is not None:
supplied_token = request.POST.get(token, "")

# If we were unable to locate a CSRF token in a request body, then we'll
# check to see if there are any headers that have a value for us.
if supplied_token == "":
if supplied_token == "" and header is not None:
supplied_token = request.headers.get(header, "")

expected_token = request.session.get_csrf_token()
Expand Down
21 changes: 21 additions & 0 deletions pyramid/tests/test_config/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,24 @@ def test_add_permission(self):
intr = D['introspectable']
self.assertEqual(intr['value'], 'perm')

def test_set_default_csrf_options(self):
from pyramid.interfaces import IDefaultCSRFOptions
config = self._makeOne(autocommit=True)
config.set_default_csrf_options()
result = config.registry.getUtility(IDefaultCSRFOptions)
self.assertEqual(result.require_csrf, True)
self.assertEqual(result.token, 'csrf_token')
self.assertEqual(result.header, 'X-CSRF-Token')
self.assertEqual(list(sorted(result.safe_methods)),
['GET', 'HEAD', 'OPTIONS', 'TRACE'])

def test_changing_set_default_csrf_options(self):
from pyramid.interfaces import IDefaultCSRFOptions
config = self._makeOne(autocommit=True)
config.set_default_csrf_options(
require_csrf=False, token='DUMMY', header=None, safe_methods=('PUT',))
result = config.registry.getUtility(IDefaultCSRFOptions)
self.assertEqual(result.require_csrf, False)
self.assertEqual(result.token, 'DUMMY')
self.assertEqual(result.header, None)
self.assertEqual(list(sorted(result.safe_methods)), ['PUT'])
Loading

0 comments on commit fe463dd

Please sign in to comment.