summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2019-10-22 22:05:27 -0500
committerGitHub <noreply@github.com>2019-10-22 22:05:27 -0500
commit4a46827769bbe181070a74927aa4e988a4cc3112 (patch)
tree1b7de5c2b62928aea23372adb2b95b42aa5eab0c /src
parent2153b4b878d77aa0cb5b79805dd185d133c26451 (diff)
parent8b7b7cbf9058312f0bf6b044cfa388f807eff739 (diff)
downloadpyramid-4a46827769bbe181070a74927aa4e988a4cc3112.tar.gz
pyramid-4a46827769bbe181070a74927aa4e988a4cc3112.tar.bz2
pyramid-4a46827769bbe181070a74927aa4e988a4cc3112.zip
Merge pull request #3518 from mmerickel/default-allow-no-origin
add check_origin option and support an origin of null
Diffstat (limited to 'src')
-rw-r--r--src/pyramid/config/security.py17
-rw-r--r--src/pyramid/csrf.py148
-rw-r--r--src/pyramid/viewderivers.py9
3 files changed, 102 insertions, 72 deletions
diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py
index 32b4db03c..99eb5792c 100644
--- a/src/pyramid/config/security.py
+++ b/src/pyramid/config/security.py
@@ -254,6 +254,7 @@ class SecurityConfiguratorMixin(object):
token='csrf_token',
header='X-CSRF-Token',
safe_methods=('GET', 'HEAD', 'OPTIONS', 'TRACE'),
+ check_origin=True,
allow_no_origin=False,
callback=None,
):
@@ -279,8 +280,13 @@ 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.
+ ``check_origin`` is a boolean. If ``False``, the ``Origin`` and
+ ``Referer`` headers will not be validated as part of automated
+ CSRF checks.
+
+ ``allow_no_origin`` is a boolean. If ``True``, a request lacking both
+ an ``Origin`` and ``Referer`` header will pass the CSRF check. This
+ option has no effect if ``check_origin`` is ``False``.
If ``callback`` is set, it must be a callable accepting ``(request)``
and returning ``True`` if the request should be checked for a valid
@@ -298,7 +304,7 @@ class SecurityConfiguratorMixin(object):
Added the ``callback`` option.
.. versionchanged:: 2.0
- Added the ``allow_no_origin`` option.
+ Added the ``allow_no_origin`` and ``check_origin`` options.
"""
options = DefaultCSRFOptions(
@@ -306,6 +312,7 @@ class SecurityConfiguratorMixin(object):
token=token,
header=header,
safe_methods=safe_methods,
+ check_origin=check_origin,
allow_no_origin=allow_no_origin,
callback=callback,
)
@@ -323,6 +330,8 @@ class SecurityConfiguratorMixin(object):
intr['token'] = token
intr['header'] = header
intr['safe_methods'] = as_sorted_tuple(safe_methods)
+ intr['check_origin'] = allow_no_origin
+ intr['allow_no_origin'] = check_origin
intr['callback'] = callback
self.action(
@@ -362,6 +371,7 @@ class DefaultCSRFOptions(object):
token,
header,
safe_methods,
+ check_origin,
allow_no_origin,
callback,
):
@@ -369,5 +379,6 @@ class DefaultCSRFOptions(object):
self.token = token
self.header = header
self.safe_methods = frozenset(safe_methods)
+ self.check_origin = check_origin
self.allow_no_origin = allow_no_origin
self.callback = callback
diff --git a/src/pyramid/csrf.py b/src/pyramid/csrf.py
index b352ada71..f9914e852 100644
--- a/src/pyramid/csrf.py
+++ b/src/pyramid/csrf.py
@@ -248,7 +248,7 @@ def check_csrf_token(
def check_csrf_origin(
- request, trusted_origins=None, allow_no_origin=False, raises=True
+ 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
@@ -266,6 +266,10 @@ def check_csrf_origin(
(the default) this list of additional domains will be pulled from the
``pyramid.csrf_trusted_origins`` setting.
+ ``allow_no_origin`` determines whether to return ``True`` when the
+ origin cannot be determined via either the ``Referer`` or ``Origin``
+ header. The default is ``False`` which will reject the check.
+
Note that this function will do nothing if ``request.scheme`` is not
``https``.
@@ -274,78 +278,90 @@ def check_csrf_origin(
.. versionchanged:: 1.9
Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf`
+ .. versionchanged:: 2.0
+ Added the ``allow_no_origin`` option.
+
"""
def _fail(reason):
if raises:
- raise BadCSRFOrigin(reason)
+ raise BadCSRFOrigin("Origin checking failed - " + reason)
else:
return False
- if request.scheme == "https":
- # Suppose user visits http://example.com/
- # An active network attacker (man-in-the-middle, MITM) sends a
- # POST form that targets https://example.com/detonate-bomb/ and
- # submits it via JavaScript.
- #
- # The attacker will need to provide a CSRF cookie and token, but
- # that's no problem for a MITM when we cannot make any assumptions
- # about what kind of session storage is being used. So the MITM can
- # circumvent the CSRF protection. This is true for any HTTP connection,
- # but anyone using HTTPS expects better! For this reason, for
- # https://example.com/ we need additional protection that treats
- # http://example.com/ as completely untrusted. Under HTTPS,
- # Barth et al. found that the Referer header is missing for
- # same-domain requests in only about 0.2% of cases or less, so
- # we can use strict Referer checking.
-
- # Determine the origin of this request
- origin = request.headers.get("Origin")
- if origin is None:
- origin = request.referrer
-
- # If we can't find an origin, fail or pass immediately depending on
- # ``allow_no_origin``
- if not origin:
- 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.
- originp = urlparse(origin)
-
- # Ensure that our Referer is also secure.
- if originp.scheme != "https":
- return _fail(
- "Referer checking failed - Referer is insecure while host is "
- "secure."
- )
-
- # Determine which origins we trust, which by default will include the
- # current origin.
- if trusted_origins is None:
- trusted_origins = aslist(
- request.registry.settings.get(
- "pyramid.csrf_trusted_origins", []
- )
- )
-
- if request.host_port not in set(["80", "443"]):
- trusted_origins.append("{0.domain}:{0.host_port}".format(request))
+ # Origin checks are only trustworthy / useful on HTTPS requests.
+ if request.scheme != "https":
+ return True
+
+ # Suppose user visits http://example.com/
+ # An active network attacker (man-in-the-middle, MITM) sends a
+ # POST form that targets https://example.com/detonate-bomb/ and
+ # submits it via JavaScript.
+ #
+ # The attacker will need to provide a CSRF cookie and token, but
+ # that's no problem for a MITM when we cannot make any assumptions
+ # about what kind of session storage is being used. So the MITM can
+ # circumvent the CSRF protection. This is true for any HTTP connection,
+ # but anyone using HTTPS expects better! For this reason, for
+ # https://example.com/ we need additional protection that treats
+ # http://example.com/ as completely untrusted. Under HTTPS,
+ # Barth et al. found that the Referer header is missing for
+ # same-domain requests in only about 0.2% of cases or less, so
+ # we can use strict Referer checking.
+
+ # Determine the origin of this request
+ origin = request.headers.get("Origin")
+ origin_is_referrer = False
+ if origin is None:
+ origin = request.referrer
+ origin_is_referrer = True
+
+ else:
+ # use the last origin in the list under the assumption that the
+ # server generally appends values and we want the origin closest
+ # to us
+ origin = origin.split(' ')[-1]
+
+ # If we can't find an origin, fail or pass immediately depending on
+ # ``allow_no_origin``
+ if not origin:
+ if allow_no_origin:
+ return True
else:
- trusted_origins.append(request.domain)
-
- # Actually check to see if the request's origin matches any of our
- # trusted origins.
- if not any(
- is_same_domain(originp.netloc, host) for host in trusted_origins
- ):
- reason = (
- "Referer checking failed - {0} does not match any trusted "
- "origins."
- )
- return _fail(reason.format(origin))
+ return _fail("missing Origin or Referer.")
+
+ # Determine which origins we trust, which by default will include the
+ # current origin.
+ if trusted_origins is None:
+ trusted_origins = aslist(
+ request.registry.settings.get("pyramid.csrf_trusted_origins", [])
+ )
+
+ if request.host_port not in set(["80", "443"]):
+ trusted_origins.append("{0.domain}:{0.host_port}".format(request))
+ else:
+ trusted_origins.append(request.domain)
+
+ # Check "Origin: null" against trusted_origins
+ if not origin_is_referrer and origin == 'null':
+ if origin in trusted_origins:
+ return True
+ else:
+ return _fail("null does not match any trusted origins.")
+
+ # Parse our origin so we we can extract the required information from
+ # it.
+ originp = urlparse(origin)
+
+ # Ensure that our Referer is also secure.
+ if originp.scheme != "https":
+ return _fail("Origin is insecure while host is secure.")
+
+ # Actually check to see if the request's origin matches any of our
+ # trusted origins.
+ if not any(
+ is_same_domain(originp.netloc, host) for host in trusted_origins
+ ):
+ return _fail("{0} does not match any trusted origins.".format(origin))
return True
diff --git a/src/pyramid/viewderivers.py b/src/pyramid/viewderivers.py
index 95c223e61..35f9a08d2 100644
--- a/src/pyramid/viewderivers.py
+++ b/src/pyramid/viewderivers.py
@@ -484,6 +484,7 @@ def csrf_view(view, info):
token = 'csrf_token'
header = 'X-CSRF-Token'
safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
+ check_origin = True
allow_no_origin = False
callback = None
else:
@@ -491,6 +492,7 @@ def csrf_view(view, info):
token = defaults.token
header = defaults.header
safe_methods = defaults.safe_methods
+ check_origin = defaults.check_origin
allow_no_origin = defaults.allow_no_origin
callback = defaults.callback
@@ -510,9 +512,10 @@ 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, allow_no_origin=allow_no_origin
- )
+ if check_origin:
+ check_csrf_origin(
+ request, raises=True, allow_no_origin=allow_no_origin
+ )
check_csrf_token(request, token, header, raises=True)
return view(context, request)