From 8b7b7cbf9058312f0bf6b044cfa388f807eff739 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 30 Sep 2019 21:27:20 -0500 Subject: support Origin: null in csrf_trusted_origins and check_origin=False --- src/pyramid/config/security.py | 17 ++++- src/pyramid/csrf.py | 148 +++++++++++++++++++++++------------------ src/pyramid/viewderivers.py | 9 ++- 3 files changed, 102 insertions(+), 72 deletions(-) (limited to 'src') 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) -- cgit v1.2.3