summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2016-04-18 10:15:32 -0500
committerMichael Merickel <michael@merickel.org>2016-04-19 02:16:13 -0500
commitde3d0c784198796285ddc4c80e45b8082ecab9e2 (patch)
treea36ce9be865bf0d376f9388318a75d8e2c1b4b39
parent6c16fb020027fac47e4d2e335cd9e264dba8aa3b (diff)
downloadpyramid-de3d0c784198796285ddc4c80e45b8082ecab9e2.tar.gz
pyramid-de3d0c784198796285ddc4c80e45b8082ecab9e2.tar.bz2
pyramid-de3d0c784198796285ddc4c80e45b8082ecab9e2.zip
replace pyramid.require_default_csrf setting with config.set_default_csrf_options
-rw-r--r--docs/api/config.rst2
-rw-r--r--docs/narr/extconfig.rst1
-rw-r--r--docs/narr/introspector.rst25
-rw-r--r--docs/narr/sessions.rst76
-rw-r--r--pyramid/config/security.py57
-rw-r--r--pyramid/config/views.py37
-rw-r--r--pyramid/interfaces.py10
-rw-r--r--pyramid/session.py7
-rw-r--r--pyramid/tests/test_config/test_security.py21
-rw-r--r--pyramid/tests/test_viewderivers.py152
-rw-r--r--pyramid/viewderivers.py58
11 files changed, 266 insertions, 180 deletions
diff --git a/docs/api/config.rst b/docs/api/config.rst
index e083dbc68..ab3ff0fe1 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -35,6 +35,7 @@
.. automethod:: set_authentication_policy
.. automethod:: set_authorization_policy
+ .. automethod:: set_default_csrf_options
.. automethod:: set_default_permission
.. automethod:: add_permission
@@ -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
diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst
index af7d0a349..babfa0a98 100644
--- a/docs/narr/extconfig.rst
+++ b/docs/narr/extconfig.rst
@@ -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`
diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst
index 98315ac9f..c9fecd4f4 100644
--- a/docs/narr/introspector.rst
+++ b/docs/narr/introspector.rst
@@ -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
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index 7cf96ac7d..bfc908396 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -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:
@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/pyramid/config/security.py b/pyramid/config/security.py
index 81549cbfc..e387eade9 100644
--- a/pyramid/config/security.py
+++ b/pyramid/config/security.py
@@ -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
@@ -138,7 +142,6 @@ class SecurityConfiguratorMixin(object):
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
@@ -159,3 +162,55 @@ class SecurityConfiguratorMixin(object):
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)
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 6fe31fd4a..34f289fcc 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -371,24 +371,24 @@ class ViewsConfiguratorMixin(object):
.. 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.
@@ -1229,7 +1229,6 @@ class ViewsConfiguratorMixin(object):
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),
@@ -1246,6 +1245,16 @@ class ViewsConfiguratorMixin(object):
)
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,
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index 2b00752cf..c03afbd39 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -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 """
diff --git a/pyramid/session.py b/pyramid/session.py
index 811c81fb4..a3cbe5172 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -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
@@ -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()
diff --git a/pyramid/tests/test_config/test_security.py b/pyramid/tests/test_config/test_security.py
index 817f6ce02..e461bfd4a 100644
--- a/pyramid/tests/test_config/test_security.py
+++ b/pyramid/tests/test_config/test_security.py
@@ -98,3 +98,24 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase):
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'])
diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py
index 4a7a04197..e84863d69 100644
--- a/pyramid/tests/test_viewderivers.py
+++ b/pyramid/tests/test_viewderivers.py
@@ -1090,45 +1090,52 @@ class TestDeriveView(unittest.TestCase):
self.assertRaises(ConfigurationError, self.config._derive_view,
view, http_cache=(None,))
- def test_csrf_view_requires_bool_or_str_in_require_csrf(self):
- def view(request): pass
- try:
- self.config._derive_view(view, require_csrf=object())
- except ConfigurationError as ex:
- self.assertEqual(
- 'View option "require_csrf" must be a string or boolean value',
- ex.args[0])
- else: # pragma: no cover
- raise AssertionError
+ def test_csrf_view_ignores_GET(self):
+ response = DummyResponse()
+ def inner_view(request):
+ return response
+ request = self._makeRequest()
+ request.method = 'GET'
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ result = view(None, request)
+ self.assertTrue(result is response)
- def test_csrf_view_requires_bool_or_str_in_config_setting(self):
- def view(request): pass
- self.config.add_settings({'pyramid.require_default_csrf': object()})
- try:
- self.config._derive_view(view)
- except ConfigurationError as ex:
- self.assertEqual(
- 'Config setting "pyramid.require_default_csrf" must be a '
- 'string or boolean value',
- ex.args[0])
- else: # pragma: no cover
- raise AssertionError
+ def test_csrf_view_fails_with_bad_POST_header(self):
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.headers = {'X-CSRF-Token': 'bar'}
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
- def test_csrf_view_requires_header(self):
+ def test_csrf_view_passes_with_good_POST_header(self):
response = DummyResponse()
def inner_view(request):
return response
request = self._makeRequest()
request.scheme = "http"
request.method = 'POST'
- request.POST = {}
request.session = DummySession({'csrf_token': 'foo'})
request.headers = {'X-CSRF-Token': 'foo'}
view = self.config._derive_view(inner_view, require_csrf=True)
result = view(None, request)
self.assertTrue(result is response)
- def test_csrf_view_requires_param(self):
+ def test_csrf_view_fails_with_bad_POST_token(self):
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
+ request = self._makeRequest()
+ request.scheme = "http"
+ request.method = 'POST'
+ request.session = DummySession({'csrf_token': 'foo'})
+ request.POST = {'csrf_token': 'bar'}
+ view = self.config._derive_view(inner_view, require_csrf=True)
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
+
+ def test_csrf_view_passes_with_good_POST_token(self):
response = DummyResponse()
def inner_view(request):
return response
@@ -1136,8 +1143,8 @@ class TestDeriveView(unittest.TestCase):
request.scheme = "http"
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
- request.POST = {'DUMMY': 'foo'}
- view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ request.POST = {'csrf_token': 'foo'}
+ view = self.config._derive_view(inner_view, require_csrf=True)
result = view(None, request)
self.assertTrue(result is response)
@@ -1152,54 +1159,20 @@ class TestDeriveView(unittest.TestCase):
request.referrer = "https://example.com/login/"
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
- request.POST = {'DUMMY': 'foo'}
- view = self.config._derive_view(inner_view, require_csrf='DUMMY')
- result = view(None, request)
- self.assertTrue(result is response)
-
- def test_csrf_view_ignores_GET(self):
- response = DummyResponse()
- def inner_view(request):
- return response
- request = self._makeRequest()
- request.method = 'GET'
+ request.POST = {'csrf_token': 'foo'}
view = self.config._derive_view(inner_view, require_csrf=True)
result = view(None, request)
self.assertTrue(result is response)
- def test_csrf_view_fails_on_bad_POST_param(self):
- from pyramid.exceptions import BadCSRFToken
- def inner_view(request): pass
- request = self._makeRequest()
- request.scheme = "http"
- request.method = 'POST'
- request.session = DummySession({'csrf_token': 'foo'})
- request.POST = {'DUMMY': 'bar'}
- view = self.config._derive_view(inner_view, require_csrf='DUMMY')
- self.assertRaises(BadCSRFToken, lambda: view(None, request))
-
- def test_csrf_view_fails_on_bad_POST_header(self):
- from pyramid.exceptions import BadCSRFToken
- def inner_view(request): pass
- request = self._makeRequest()
- request.scheme = "http"
- request.method = 'POST'
- request.POST = {}
- request.session = DummySession({'csrf_token': 'foo'})
- request.headers = {'X-CSRF-Token': 'bar'}
- view = self.config._derive_view(inner_view, require_csrf='DUMMY')
- self.assertRaises(BadCSRFToken, lambda: view(None, request))
-
def test_csrf_view_fails_on_bad_PUT_header(self):
from pyramid.exceptions import BadCSRFToken
def inner_view(request): pass
request = self._makeRequest()
request.scheme = "http"
request.method = 'PUT'
- request.POST = {}
request.session = DummySession({'csrf_token': 'foo'})
request.headers = {'X-CSRF-Token': 'bar'}
- view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ view = self.config._derive_view(inner_view, require_csrf=True)
self.assertRaises(BadCSRFToken, lambda: view(None, request))
def test_csrf_view_fails_on_bad_referrer(self):
@@ -1212,7 +1185,7 @@ class TestDeriveView(unittest.TestCase):
request.domain = "example.com"
request.referrer = "https://not-example.com/evil/"
request.registry.settings = {}
- view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ view = self.config._derive_view(inner_view, require_csrf=True)
self.assertRaises(BadCSRFOrigin, lambda: view(None, request))
def test_csrf_view_fails_on_bad_origin(self):
@@ -1225,24 +1198,21 @@ class TestDeriveView(unittest.TestCase):
request.domain = "example.com"
request.headers = {"Origin": "https://not-example.com/evil/"}
request.registry.settings = {}
- view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ view = self.config._derive_view(inner_view, require_csrf=True)
self.assertRaises(BadCSRFOrigin, lambda: view(None, request))
- def test_csrf_view_uses_config_setting_truthy(self):
- response = DummyResponse()
- def inner_view(request):
- return response
+ def test_csrf_view_enabled_by_default(self):
+ from pyramid.exceptions import BadCSRFToken
+ def inner_view(request): pass
request = self._makeRequest()
request.scheme = "http"
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
- request.POST = {'csrf_token': 'foo'}
- self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
+ self.config.set_default_csrf_options(require_csrf=True)
view = self.config._derive_view(inner_view)
- result = view(None, request)
- self.assertTrue(result is response)
+ self.assertRaises(BadCSRFToken, lambda: view(None, request))
- def test_csrf_view_uses_config_setting_with_custom_token(self):
+ def test_csrf_view_uses_custom_csrf_token(self):
response = DummyResponse()
def inner_view(request):
return response
@@ -1251,39 +1221,40 @@ class TestDeriveView(unittest.TestCase):
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
request.POST = {'DUMMY': 'foo'}
- self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'})
+ self.config.set_default_csrf_options(require_csrf=True, token='DUMMY')
view = self.config._derive_view(inner_view)
result = view(None, request)
self.assertTrue(result is response)
- def test_csrf_view_uses_config_setting_falsey(self):
+ def test_csrf_view_uses_custom_csrf_header(self):
response = DummyResponse()
def inner_view(request):
return response
request = self._makeRequest()
+ request.scheme = "http"
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
- request.params['csrf_token'] = 'foo'
- self.config.add_settings({'pyramid.require_default_csrf': 'no'})
+ request.headers = {'DUMMY': 'foo'}
+ self.config.set_default_csrf_options(require_csrf=True, header='DUMMY')
view = self.config._derive_view(inner_view)
result = view(None, request)
self.assertTrue(result is response)
- def test_csrf_view_uses_view_option_override(self):
+ def test_csrf_view_uses_custom_methods(self):
response = DummyResponse()
def inner_view(request):
return response
request = self._makeRequest()
request.scheme = "http"
- request.method = 'POST'
+ request.method = 'PUT'
request.session = DummySession({'csrf_token': 'foo'})
- request.POST = {'DUMMY': 'foo'}
- self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
- view = self.config._derive_view(inner_view, require_csrf='DUMMY')
+ self.config.set_default_csrf_options(
+ require_csrf=True, safe_methods=['PUT'])
+ view = self.config._derive_view(inner_view)
result = view(None, request)
self.assertTrue(result is response)
- def test_csrf_view_uses_config_setting_when_view_option_is_true(self):
+ def test_csrf_view_uses_view_option_override(self):
response = DummyResponse()
def inner_view(request):
return response
@@ -1291,9 +1262,9 @@ class TestDeriveView(unittest.TestCase):
request.scheme = "http"
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
- request.POST = {'DUMMY': 'foo'}
- self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'})
- view = self.config._derive_view(inner_view, require_csrf=True)
+ request.POST = {'csrf_token': 'bar'}
+ self.config.set_default_csrf_options(require_csrf=True)
+ view = self.config._derive_view(inner_view, require_csrf=False)
result = view(None, request)
self.assertTrue(result is response)
@@ -1303,7 +1274,7 @@ class TestDeriveView(unittest.TestCase):
raise ValueError
def excview(request):
return 'hello'
- self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
+ self.config.set_default_csrf_options(require_csrf=True)
self.config.set_session_factory(
lambda request: DummySession({'csrf_token': 'foo'}))
self.config.add_view(view, name='foo', require_csrf=False)
@@ -1320,7 +1291,7 @@ class TestDeriveView(unittest.TestCase):
def view(request):
raise ValueError
def excview(request): pass
- self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
+ self.config.set_default_csrf_options(require_csrf=True)
self.config.set_session_factory(
lambda request: DummySession({'csrf_token': 'foo'}))
self.config.add_view(view, name='foo', require_csrf=False)
@@ -1342,7 +1313,7 @@ class TestDeriveView(unittest.TestCase):
raise ValueError
def excview(request):
return 'hello'
- self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
+ self.config.set_default_csrf_options(require_csrf=True)
self.config.set_session_factory(
lambda request: DummySession({'csrf_token': 'foo'}))
self.config.add_view(view, name='foo', require_csrf=False)
@@ -1675,6 +1646,7 @@ class DummyRequest:
environ = {}
self.environ = environ
self.params = {}
+ self.POST = {}
self.cookies = {}
self.headers = {}
self.response = DummyResponse()
diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py
index fbe7cd660..1b922b89e 100644
--- a/pyramid/viewderivers.py
+++ b/pyramid/viewderivers.py
@@ -15,6 +15,7 @@ from pyramid.response import Response
from pyramid.interfaces import (
IAuthenticationPolicy,
IAuthorizationPolicy,
+ IDefaultCSRFOptions,
IDebugLogger,
IResponse,
IViewMapper,
@@ -22,7 +23,6 @@ from pyramid.interfaces import (
)
from pyramid.compat import (
- string_types,
is_bound_method,
is_unbound_method,
)
@@ -38,10 +38,6 @@ from pyramid.exceptions import (
PredicateMismatch,
)
from pyramid.httpexceptions import HTTPForbidden
-from pyramid.settings import (
- falsey,
- truthy,
-)
from pyramid.util import object_description
from pyramid.view import render_view_to_response
from pyramid import renderers
@@ -464,40 +460,30 @@ def decorated_view(view, info):
decorated_view.options = ('decorator',)
-def _parse_csrf_setting(val, error_source):
- if val:
- if isinstance(val, string_types):
- if val.lower() in truthy:
- val = True
- elif val.lower() in falsey:
- val = False
- elif not isinstance(val, bool):
- raise ConfigurationError(
- '{0} must be a string or boolean value'
- .format(error_source))
- return val
-
-SAFE_REQUEST_METHODS = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
-
def csrf_view(view, info):
- default_val = _parse_csrf_setting(
- info.settings.get('pyramid.require_default_csrf'),
- 'Config setting "pyramid.require_default_csrf"')
- explicit_val = _parse_csrf_setting(
- info.options.get('require_csrf'),
- 'View option "require_csrf"')
- resolved_val = explicit_val
- if (explicit_val is True and default_val) or explicit_val is None:
- resolved_val = default_val
- if resolved_val is True:
- resolved_val = 'csrf_token'
+ explicit_val = info.options.get('require_csrf')
+ defaults = info.registry.queryUtility(IDefaultCSRFOptions)
+ if defaults is None:
+ default_val = False
+ token = 'csrf_token'
+ header = 'X-CSRF-Token'
+ safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
+ else:
+ default_val = defaults.require_csrf
+ token = defaults.token
+ header = defaults.header
+ safe_methods = defaults.safe_methods
+ enabled = (
+ explicit_val is True or
+ (explicit_val is not False and default_val)
+ )
+ # disable if both header and token are disabled
+ enabled = enabled and (token or header)
wrapped_view = view
- if resolved_val:
+ if enabled:
def csrf_view(context, request):
- # Assume that anything not defined as 'safe' by RFC2616 needs
- # protection
if (
- request.method not in SAFE_REQUEST_METHODS and
+ request.method not in safe_methods and
(
# skip exception views unless value is explicitly defined
getattr(request, 'exception', None) is None or
@@ -505,7 +491,7 @@ def csrf_view(view, info):
)
):
check_csrf_origin(request, raises=True)
- check_csrf_token(request, resolved_val, raises=True)
+ check_csrf_token(request, token, header, raises=True)
return view(context, request)
wrapped_view = csrf_view
return wrapped_view