summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/narr/security.rst4
-rw-r--r--src/pyramid/config/security.py25
-rw-r--r--src/pyramid/csrf.py12
-rw-r--r--src/pyramid/interfaces.py4
-rw-r--r--src/pyramid/viewderivers.py6
-rw-r--r--tests/test_config/test_security.py3
-rw-r--r--tests/test_csrf.py6
-rw-r--r--tests/test_viewderivers.py21
8 files changed, 74 insertions, 7 deletions
diff --git a/docs/narr/security.rst b/docs/narr/security.rst
index 94469ba48..2b0a2f032 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 ``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 08e7cb81a..17ac5ded7 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,
):
"""
@@ -221,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
@@ -236,9 +240,17 @@ class SecurityConfiguratorMixin(object):
.. versionchanged:: 1.8
Added the ``callback`` option.
+ .. versionchanged:: 2.0
+ Added the ``allow_no_origin`` option.
+
"""
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 +299,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