From 6dd21309e4d9b21162b8db3e015533be10db0601 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Thu, 19 Sep 2019 18:32:41 -0700 Subject: Add allow_no_origin option to CSRF. --- src/pyramid/config/security.py | 19 +++++++++++++++++-- src/pyramid/csrf.py | 12 +++++++++--- src/pyramid/interfaces.py | 4 ++++ src/pyramid/viewderivers.py | 6 +++++- tests/test_config/test_security.py | 3 +++ tests/test_csrf.py | 6 ++++++ tests/test_viewderivers.py | 21 +++++++++++++++++++++ 7 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index 08e7cb81a..0d2bc8e99 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -197,6 +197,7 @@ class SecurityConfiguratorMixin(object): token='csrf_token', header='X-CSRF-Token', safe_methods=('GET', 'HEAD', 'OPTIONS', 'TRACE'), + allow_no_origin=False, callback=None, ): """ @@ -238,7 +239,12 @@ class SecurityConfiguratorMixin(object): """ options = DefaultCSRFOptions( - require_csrf, token, header, safe_methods, callback + require_csrf=require_csrf, + token=token, + header=header, + safe_methods=safe_methods, + allow_no_origin=allow_no_origin, + callback=callback, ) def register(): @@ -287,9 +293,18 @@ class SecurityConfiguratorMixin(object): @implementer(IDefaultCSRFOptions) class DefaultCSRFOptions(object): - def __init__(self, require_csrf, token, header, safe_methods, callback): + def __init__( + self, + require_csrf, + token, + header, + safe_methods, + allow_no_origin, + callback, + ): self.require_csrf = require_csrf self.token = token self.header = header self.safe_methods = frozenset(safe_methods) + self.allow_no_origin = allow_no_origin self.callback = callback diff --git a/src/pyramid/csrf.py b/src/pyramid/csrf.py index deb35fedb..b352ada71 100644 --- a/src/pyramid/csrf.py +++ b/src/pyramid/csrf.py @@ -247,7 +247,9 @@ def check_csrf_token( return True -def check_csrf_origin(request, trusted_origins=None, raises=True): +def check_csrf_origin( + request, trusted_origins=None, allow_no_origin=False, raises=True +): """ Check the ``Origin`` of the request to see if it is a cross site request or not. @@ -302,9 +304,13 @@ def check_csrf_origin(request, trusted_origins=None, raises=True): if origin is None: origin = request.referrer - # Fail if we were not able to locate an origin at all + # If we can't find an origin, fail or pass immediately depending on + # ``allow_no_origin`` if not origin: - return _fail("Origin checking failed - no Origin or Referer.") + if allow_no_origin: + return True + else: + return _fail("Origin checking failed - no Origin or Referer.") # Parse our origin so we we can extract the required information from # it. diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py index 638c1a9fd..15ae3faaa 100644 --- a/src/pyramid/interfaces.py +++ b/src/pyramid/interfaces.py @@ -1055,6 +1055,10 @@ class IDefaultCSRFOptions(Interface): header = Attribute('The header to be matched with the CSRF token.') safe_methods = Attribute('A set of safe methods that skip CSRF checks.') callback = Attribute('A callback to disable CSRF checks per-request.') + allow_no_origin = Attribute( + 'Boolean. If false, a request lacking both an ``Origin`` and ' + '``Referer`` header will fail the CSRF check.' + ) class ISessionFactory(Interface): diff --git a/src/pyramid/viewderivers.py b/src/pyramid/viewderivers.py index 181cc9e5c..c41a57d7e 100644 --- a/src/pyramid/viewderivers.py +++ b/src/pyramid/viewderivers.py @@ -488,12 +488,14 @@ def csrf_view(view, info): token = 'csrf_token' header = 'X-CSRF-Token' safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"]) + allow_no_origin = False callback = None else: default_val = defaults.require_csrf token = defaults.token header = defaults.header safe_methods = defaults.safe_methods + allow_no_origin = defaults.allow_no_origin callback = defaults.callback enabled = ( @@ -512,7 +514,9 @@ def csrf_view(view, info): if request.method not in safe_methods and ( callback is None or callback(request) ): - check_csrf_origin(request, raises=True) + check_csrf_origin( + request, raises=True, allow_no_origin=allow_no_origin + ) check_csrf_token(request, token, header, raises=True) return view(context, request) diff --git a/tests/test_config/test_security.py b/tests/test_config/test_security.py index 5ebd78f8d..6257960b8 100644 --- a/tests/test_config/test_security.py +++ b/tests/test_config/test_security.py @@ -126,6 +126,7 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase): list(sorted(result.safe_methods)), ['GET', 'HEAD', 'OPTIONS', 'TRACE'], ) + self.assertFalse(result.allow_no_origin) self.assertTrue(result.callback is None) def test_changing_set_default_csrf_options(self): @@ -141,6 +142,7 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase): token='DUMMY', header=None, safe_methods=('PUT',), + allow_no_origin=True, callback=callback, ) result = config.registry.getUtility(IDefaultCSRFOptions) @@ -148,4 +150,5 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase): self.assertEqual(result.token, 'DUMMY') self.assertEqual(result.header, None) self.assertEqual(list(sorted(result.safe_methods)), ['PUT']) + self.assertTrue(result.allow_no_origin) self.assertTrue(result.callback is callback) diff --git a/tests/test_csrf.py b/tests/test_csrf.py index d1b569c32..f93a1afde 100644 --- a/tests/test_csrf.py +++ b/tests/test_csrf.py @@ -363,6 +363,12 @@ class Test_check_csrf_origin(unittest.TestCase): request.registry.settings = {} self.assertTrue(self._callFUT(request)) + def test_success_with_allow_no_origin(self): + request = testing.DummyRequest() + request.scheme = "https" + request.referrer = None + self.assertTrue(self._callFUT(request, allow_no_origin=True)) + def test_fails_with_wrong_host(self): from pyramid.exceptions import BadCSRFOrigin diff --git a/tests/test_viewderivers.py b/tests/test_viewderivers.py index f01cb490e..3ca5f8534 100644 --- a/tests/test_viewderivers.py +++ b/tests/test_viewderivers.py @@ -1504,6 +1504,27 @@ class TestDeriveView(unittest.TestCase): result = view(None, request) self.assertTrue(result is response) + def test_csrf_view_allow_no_origin(self): + response = DummyResponse() + + def inner_view(request): + return response + + self.config.set_default_csrf_options( + require_csrf=True, allow_no_origin=True + ) + request = self._makeRequest() + request.scheme = "https" + request.domain = "example.com" + request.host_port = "443" + request.referrer = None + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + 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_PUT_header(self): from pyramid.exceptions import BadCSRFToken -- cgit v1.2.3 From 9ffed1017d5e416813df73e4e76b6bfd1d2da2c8 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Thu, 19 Sep 2019 20:30:08 -0700 Subject: Document CSRF allow_no_origin option. --- docs/narr/security.rst | 4 +++- src/pyramid/config/security.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 94469ba48..f6794dc2c 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -944,7 +944,9 @@ 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 ``.``. +that will allow all subdomains as well as the domain without the ``.``. If no +``Referer`` or ``Origin`` header is present in an HTTPS request, the CSRF check +will fail unless the ``allow_no_origin`` is set. If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` or :class:`pyramid.exceptions.BadCSRFOrigin` exception will be raised. This diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index 0d2bc8e99..02271e2ba 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -222,6 +222,9 @@ class SecurityConfiguratorMixin(object): never be automatically checked for CSRF tokens. Default: ``('GET', 'HEAD', 'OPTIONS', TRACE')``. + ``allow_no_origin`` is a boolean. If false, a request lacking both an + ``Origin`` and ``Referer`` header will fail the CSRF check.' + If ``callback`` is set, it must be a callable accepting ``(request)`` and returning ``True`` if the request should be checked for a valid CSRF token. This callback allows an application to support @@ -237,6 +240,9 @@ class SecurityConfiguratorMixin(object): .. versionchanged:: 1.8 Added the ``callback`` option. + .. versionchanged:: 2.0 + Added the ``allow_no_origin`` option. + """ options = DefaultCSRFOptions( require_csrf=require_csrf, -- cgit v1.2.3 From 78db10b672bf91185452e68c2b695c3d4e0272ce Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Thu, 19 Sep 2019 20:47:54 -0700 Subject: Add a whatsnew-2.0 doc. --- docs/index.rst | 1 + docs/whatsnew-2.0.rst | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 docs/whatsnew-2.0.rst diff --git a/docs/index.rst b/docs/index.rst index 09a3b56b0..c1f6db81a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -174,6 +174,7 @@ Change History .. toctree:: :maxdepth: 1 + whatsnew-2.0 whatsnew-1.10 whatsnew-1.9 whatsnew-1.8 diff --git a/docs/whatsnew-2.0.rst b/docs/whatsnew-2.0.rst new file mode 100644 index 000000000..fd7c69000 --- /dev/null +++ b/docs/whatsnew-2.0.rst @@ -0,0 +1,16 @@ +What's New in Pyramid 2.0 +========================= + +This article explains the new features in :app:`Pyramid` version 2.0 as +compared to its predecessor, :app:`Pyramid` 1.10. It also documents backwards +incompatibilities between the two versions and deprecations added to +:app:`Pyramid` 2.0, as well as software dependency changes and notable +documentation additions. + +Feature Additions +----------------- + +The feature additions in Pyramid 2.0 are as follows: + +- Added ``allow_no_origin`` option to :meth:`pyramid.config.Configurator.set_default_csrf_options`. + See https://github.com/Pylons/pyramid/pull/3512 -- cgit v1.2.3 From 904314e683cc488871ba8f163ff47a5c3be86db4 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 23 Sep 2019 11:02:56 -0700 Subject: Doc fixes from @Deimos --- docs/narr/security.rst | 2 +- src/pyramid/config/security.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index f6794dc2c..2b0a2f032 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -946,7 +946,7 @@ is the current host, however additional origins may be configured by setting 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 no ``Referer`` or ``Origin`` header is present in an HTTPS request, the CSRF check -will fail unless the ``allow_no_origin`` is set. +will fail unless ``allow_no_origin`` is set. If CSRF checks fail then a :class:`pyramid.exceptions.BadCSRFToken` or :class:`pyramid.exceptions.BadCSRFOrigin` exception will be raised. This diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py index 02271e2ba..17ac5ded7 100644 --- a/src/pyramid/config/security.py +++ b/src/pyramid/config/security.py @@ -223,7 +223,7 @@ class SecurityConfiguratorMixin(object): Default: ``('GET', 'HEAD', 'OPTIONS', TRACE')``. ``allow_no_origin`` is a boolean. If false, a request lacking both an - ``Origin`` and ``Referer`` header will fail the CSRF check.' + ``Origin`` and ``Referer`` header will fail the CSRF check. If ``callback`` is set, it must be a callable accepting ``(request)`` and returning ``True`` if the request should be checked for a valid -- cgit v1.2.3 From 070642056a2863c5da20cbc28626f4e8e1c49cdb Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Fri, 27 Sep 2019 12:41:42 -0700 Subject: Remove whatsnew-2.0 --- docs/index.rst | 1 - docs/whatsnew-2.0.rst | 16 ---------------- 2 files changed, 17 deletions(-) delete mode 100644 docs/whatsnew-2.0.rst diff --git a/docs/index.rst b/docs/index.rst index c1f6db81a..09a3b56b0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -174,7 +174,6 @@ Change History .. toctree:: :maxdepth: 1 - whatsnew-2.0 whatsnew-1.10 whatsnew-1.9 whatsnew-1.8 diff --git a/docs/whatsnew-2.0.rst b/docs/whatsnew-2.0.rst deleted file mode 100644 index fd7c69000..000000000 --- a/docs/whatsnew-2.0.rst +++ /dev/null @@ -1,16 +0,0 @@ -What's New in Pyramid 2.0 -========================= - -This article explains the new features in :app:`Pyramid` version 2.0 as -compared to its predecessor, :app:`Pyramid` 1.10. It also documents backwards -incompatibilities between the two versions and deprecations added to -:app:`Pyramid` 2.0, as well as software dependency changes and notable -documentation additions. - -Feature Additions ------------------ - -The feature additions in Pyramid 2.0 are as follows: - -- Added ``allow_no_origin`` option to :meth:`pyramid.config.Configurator.set_default_csrf_options`. - See https://github.com/Pylons/pyramid/pull/3512 -- cgit v1.2.3