diff options
| -rw-r--r-- | CHANGES.txt | 12 | ||||
| -rw-r--r-- | docs/api/csrf.rst | 18 | ||||
| -rw-r--r-- | docs/api/interfaces.rst | 3 | ||||
| -rw-r--r-- | docs/api/session.rst | 4 | ||||
| -rw-r--r-- | docs/narr/security.rst | 191 | ||||
| -rw-r--r-- | docs/narr/sessions.rst | 175 | ||||
| -rw-r--r-- | pyramid/config/security.py | 16 | ||||
| -rw-r--r-- | pyramid/config/views.py | 14 | ||||
| -rw-r--r-- | pyramid/csrf.py | 286 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 29 | ||||
| -rw-r--r-- | pyramid/predicates.py | 2 | ||||
| -rw-r--r-- | pyramid/renderers.py | 1 | ||||
| -rw-r--r-- | pyramid/session.py | 155 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 2 | ||||
| -rw-r--r-- | pyramid/tests/test_csrf.py | 172 | ||||
| -rw-r--r-- | pyramid/tests/test_session.py | 4 | ||||
| -rw-r--r-- | pyramid/viewderivers.py | 2 |
17 files changed, 731 insertions, 355 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index c8a87f625..9d6264688 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -24,6 +24,14 @@ Features can be alleviated by invoking ``config.begin()`` and ``config.end()`` appropriately. See https://github.com/Pylons/pyramid/pull/2989 +- A new CSRF implementation, :class:`pyramid.csrf.SessionCSRF` has been added, + which deleagates all CSRF generation to the current session, following the + old API for this. A ``get_csrf_token()`` method is now available in template + global scope, to make it easy for template developers to get the current CSRF + token without adding it to Python code. + See https://github.com/Pylons/pyramid/pull/2854 + + Bug Fixes --------- @@ -50,3 +58,7 @@ Backward Incompatibilities Documentation Changes --------------------- + +- Retrieving CSRF token from the session has been deprecated, in favor of + equivalent methods in :mod:`pyramid.csrf`. + See https://github.com/Pylons/pyramid/pull/2854 diff --git a/docs/api/csrf.rst b/docs/api/csrf.rst new file mode 100644 index 000000000..3125bdac9 --- /dev/null +++ b/docs/api/csrf.rst @@ -0,0 +1,18 @@ +.. _csrf_module: + +:mod:`pyramid.csrf` +------------------- + +.. automodule:: pyramid.csrf + + .. autofunction:: get_csrf_token + + .. autofunction:: new_csrf_token + + .. autoclass:: SessionCSRF + :members: + + .. autofunction:: check_csrf_origin + + .. autofunction:: check_csrf_token + diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index a212ba7a9..2ca472616 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -44,6 +44,9 @@ Other Interfaces .. autointerface:: IRoutePregenerator :members: + .. autointerface:: ICSRF + :members: + .. autointerface:: ISession :members: diff --git a/docs/api/session.rst b/docs/api/session.rst index 56c4f52d7..53bae7c52 100644 --- a/docs/api/session.rst +++ b/docs/api/session.rst @@ -9,10 +9,6 @@ .. autofunction:: signed_deserialize - .. autofunction:: check_csrf_origin - - .. autofunction:: check_csrf_token - .. autofunction:: SignedCookieSessionFactory .. autofunction:: UnencryptedCookieSessionFactoryConfig diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 77e7fd707..b4fb3b8a8 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -765,3 +765,194 @@ which would allow the attacker to control the content of the payload. Re-using a secret across two different subsystems might drop the security of signing to zero. Keys should not be re-used across different contexts where an attacker has the possibility of providing a chosen plaintext. + +Preventing Cross-Site Request Forgery Attacks +--------------------------------------------- + +`Cross-site request forgery +<https://en.wikipedia.org/wiki/Cross-site_request_forgery>`_ attacks are a +phenomenon whereby a user who is logged in to your website might inadvertantly +load a URL because it is linked from, or embedded in, an attacker's website. +If the URL is one that may modify or delete data, the consequences can be dire. + +You can avoid most of these attacks by issuing a unique token to the browser +and then requiring that it be present in all potentially unsafe requests. +:app:`Pyramid` sessions provide facilities to create and check CSRF tokens. + +To use CSRF tokens, you must first enable a :term:`session factory` as +described in :ref:`using_the_default_session_factory` or +:ref:`using_alternate_session_factories`. + +.. index:: + single: csrf.get_csrf_token + +Using the ``csrf.get_csrf_token`` Method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To get the current CSRF token, use the +:data:`pyramid.csrf.get_csrf_token` method. + +.. code-block:: python + + from pyramid.csrf import get_csrf_token + token = get_csrf_token(request) + +The ``get_csrf_token()`` method accepts a single argument: the request. It +returns a CSRF *token* string. If ``get_csrf_token()`` or ``new_csrf_token()`` +was invoked previously for this user, then the existing token will be returned. +If no CSRF token previously existed for this user, then a new token will be set +into the session and returned. The newly created token will be opaque and +randomized. + + +Using the ``get_csrf_token`` global in templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Templates have a ``get_csrf_token()`` method inserted into their globals, which +allows you to get the current token without modifying the view code. This +method takes no arguments and returns a CSRF token string. You can use the +returned token as the value of a hidden field in a form that posts to a method +that requires elevated privileges, or supply it as a request header in AJAX +requests. + +For example, include the CSRF token as a hidden field: + +.. code-block:: html + + <form method="post" action="/myview"> + <input type="hidden" name="csrf_token" value="${get_csrf_token()}"> + <input type="submit" value="Delete Everything"> + </form> + +Or include it as a header in a jQuery AJAX request: + +.. code-block:: javascript + + var csrfToken = "${get_csrf_token()}"; + $.ajax({ + type: "POST", + url: "/myview", + headers: { 'X-CSRF-Token': csrfToken } + }).done(function() { + alert("Deleted"); + }); + +The handler for the URL that receives the request should then require that the +correct CSRF token is supplied. + +.. index:: + single: csrf.new_csrf_token + +Using the ``csrf.new_csrf_token`` Method +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To explicitly create a new CSRF token, use the ``csrf.new_csrf_token()`` +method. This differs only from ``csrf.get_csrf_token()`` inasmuch as it +clears any existing CSRF token, creates a new CSRF token, sets the token into +the user, and returns the token. + +.. code-block:: python + + from pyramid.csrf import get_csrf_token + token = new_csrf_token() + +.. note:: + + It is not possible to force a new CSRF token from a template. If you + want to regenerate your CSRF token then do it in the view code and return + the new token as part of the context. + +Checking CSRF Tokens Manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In request handling code, you can check the presence and validity of a CSRF +token with :func:`pyramid.session.check_csrf_token`. If the token is valid, it +will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally, +you can specify ``raises=False`` to have the check return ``False`` instead of +raising an exception. + +By default, it checks for a POST parameter named ``csrf_token`` or a header +named ``X-CSRF-Token``. + +.. code-block:: python + + from pyramid.session import check_csrf_token + + def myview(request): + # Require CSRF Token + check_csrf_token(request) + + # ... + +.. _auto_csrf_checking: + +Checking CSRF Tokens Automatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.7 + +: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 +: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` 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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 1.7 + Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead + to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. + +A convenient way to require a valid CSRF token for a particular view is to +include ``check_csrf=True`` as a view predicate. See +:meth:`pyramid.config.Configurator.add_view`. + +.. code-block:: python + + @view_config(request_method='POST', check_csrf=True, ...) + def myview(request): + ... + +.. note:: + A mismatch of a CSRF token is treated like any other predicate miss, and the + predicate system, when it doesn't find a view, raises ``HTTPNotFound`` + instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different + from calling :func:`pyramid.session.check_csrf_token`. diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 5b24201a9..90b5f4585 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -321,178 +321,3 @@ flash storage. single: preventing cross-site request forgery attacks single: cross-site request forgery attacks, prevention -Preventing Cross-Site Request Forgery Attacks ---------------------------------------------- - -`Cross-site request forgery -<https://en.wikipedia.org/wiki/Cross-site_request_forgery>`_ attacks are a -phenomenon whereby a user who is logged in to your website might inadvertantly -load a URL because it is linked from, or embedded in, an attacker's website. -If the URL is one that may modify or delete data, the consequences can be dire. - -You can avoid most of these attacks by issuing a unique token to the browser -and then requiring that it be present in all potentially unsafe requests. -:app:`Pyramid` sessions provide facilities to create and check CSRF tokens. - -To use CSRF tokens, you must first enable a :term:`session factory` as -described in :ref:`using_the_default_session_factory` or -:ref:`using_alternate_session_factories`. - -.. index:: - single: session.get_csrf_token - -Using the ``session.get_csrf_token`` Method -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To get the current CSRF token from the session, use the -``session.get_csrf_token()`` method. - -.. code-block:: python - - token = request.session.get_csrf_token() - -The ``session.get_csrf_token()`` method accepts no arguments. It returns a -CSRF *token* string. If ``session.get_csrf_token()`` or -``session.new_csrf_token()`` was invoked previously for this session, then the -existing token will be returned. If no CSRF token previously existed for this -session, then a new token will be set into the session and returned. The newly -created token will be opaque and randomized. - -You can use the returned token as the value of a hidden field in a form that -posts to a method that requires elevated privileges, or supply it as a request -header in AJAX requests. - -For example, include the CSRF token as a hidden field: - -.. code-block:: html - - <form method="post" action="/myview"> - <input type="hidden" name="csrf_token" value="${request.session.get_csrf_token()}"> - <input type="submit" value="Delete Everything"> - </form> - -Or include it as a header in a jQuery AJAX request: - -.. code-block:: javascript - - var csrfToken = ${request.session.get_csrf_token()}; - $.ajax({ - type: "POST", - url: "/myview", - headers: { 'X-CSRF-Token': csrfToken } - }).done(function() { - alert("Deleted"); - }); - -The handler for the URL that receives the request should then require that the -correct CSRF token is supplied. - -.. index:: - single: session.new_csrf_token - -Using the ``session.new_csrf_token`` Method -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To explicitly create a new CSRF token, use the ``session.new_csrf_token()`` -method. This differs only from ``session.get_csrf_token()`` inasmuch as it -clears any existing CSRF token, creates a new CSRF token, sets the token into -the session, and returns the token. - -.. code-block:: python - - token = request.session.new_csrf_token() - -Checking CSRF Tokens Manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In request handling code, you can check the presence and validity of a CSRF -token with :func:`pyramid.session.check_csrf_token`. If the token is valid, it -will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally, -you can specify ``raises=False`` to have the check return ``False`` instead of -raising an exception. - -By default, it checks for a POST parameter named ``csrf_token`` or a header -named ``X-CSRF-Token``. - -.. code-block:: python - - from pyramid.session import check_csrf_token - - def myview(request): - # Require CSRF Token - check_csrf_token(request) - - # ... - -.. _auto_csrf_checking: - -Checking CSRF Tokens Automatically -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. versionadded:: 1.7 - -: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 -: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` 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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 1.7 - Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead - to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. - -A convenient way to require a valid CSRF token for a particular view is to -include ``check_csrf=True`` as a view predicate. See -:meth:`pyramid.config.Configurator.add_view`. - -.. code-block:: python - - @view_config(request_method='POST', check_csrf=True, ...) - def myview(request): - ... - -.. note:: - A mismatch of a CSRF token is treated like any other predicate miss, and the - predicate system, when it doesn't find a view, raises ``HTTPNotFound`` - instead of ``HTTPBadRequest``, so ``check_csrf=True`` behavior is different - from calling :func:`pyramid.session.check_csrf_token`. diff --git a/pyramid/config/security.py b/pyramid/config/security.py index 33593376b..102a61e0c 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -3,16 +3,21 @@ from zope.interface import implementer from pyramid.interfaces import ( IAuthorizationPolicy, IAuthenticationPolicy, + ICSRFPolicy, IDefaultCSRFOptions, IDefaultPermission, PHASE1_CONFIG, PHASE2_CONFIG, ) +from pyramid.csrf import csrf_token_template_global +from pyramid.csrf import SessionCSRF +from pyramid.events import BeforeRender from pyramid.exceptions import ConfigurationError from pyramid.util import action_method from pyramid.util import as_sorted_tuple + class SecurityConfiguratorMixin(object): @action_method def set_authentication_policy(self, policy): @@ -165,6 +170,7 @@ class SecurityConfiguratorMixin(object): @action_method def set_default_csrf_options( self, + implementation=None, require_csrf=True, token='csrf_token', header='X-CSRF-Token', @@ -174,6 +180,10 @@ class SecurityConfiguratorMixin(object): """ Set the default CSRF options used by subsequent view registrations. + ``implementation`` is a class that implements the + :meth:`pyramid.interfaces.ICSRFPolicy` interface that will be used for all + CSRF functionality. Default: :class:`pyramid.csrf.SessionCSRF`. + ``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 @@ -207,7 +217,10 @@ class SecurityConfiguratorMixin(object): options = DefaultCSRFOptions( require_csrf, token, header, safe_methods, callback, ) + if implementation is None: + implementation = SessionCSRF() def register(): + self.registry.registerUtility(implementation, ICSRFPolicy) self.registry.registerUtility(options, IDefaultCSRFOptions) intr = self.introspectable('default csrf view options', None, @@ -218,9 +231,12 @@ class SecurityConfiguratorMixin(object): intr['header'] = header intr['safe_methods'] = as_sorted_tuple(safe_methods) intr['callback'] = callback + + self.add_subscriber(csrf_token_template_global, [BeforeRender]) self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG, introspectables=(intr,)) + @implementer(IDefaultCSRFOptions) class DefaultCSRFOptions(object): def __init__(self, require_csrf, token, header, safe_methods, callback): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 65c9da585..7a383be44 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -641,16 +641,14 @@ class ViewsConfiguratorMixin(object): 'check name'. If the value provided is ``True``, ``csrf_token`` will be used as the check name. - If CSRF checking is performed, the checked value will be the value - of ``request.params[check_name]``. This value will be compared - against the value of ``request.session.get_csrf_token()``, and the - check will pass if these two values are the same. If the check - passes, the associated view will be permitted to execute. If the + If CSRF checking is performed, the checked value will be the value of + ``request.params[check_name]``. This value will be compared against + the value of ``impl.get_csrf_token()`` (where ``impl`` is an + implementation of :meth:`pyramid.interfaces.ICSRFPolicy`), and the + check will pass if these two values are the same. If the check + passes, the associated view will be permitted to execute. If the check fails, the associated view will not be permitted to execute. - Note that using this feature requires a :term:`session factory` to - have been configured. - .. versionadded:: 1.4a2 physical_path diff --git a/pyramid/csrf.py b/pyramid/csrf.py new file mode 100644 index 000000000..c373079a4 --- /dev/null +++ b/pyramid/csrf.py @@ -0,0 +1,286 @@ +from functools import partial +import uuid + +from zope.interface import implementer + +from pyramid.compat import ( + urlparse, + bytes_ +) +from pyramid.exceptions import ( + BadCSRFOrigin, + BadCSRFToken, +) +from pyramid.interfaces import ICSRFPolicy +from pyramid.settings import aslist +from pyramid.util import ( + is_same_domain, + strings_differ +) + + +@implementer(ICSRFPolicy) +class SessionCSRF(object): + """ The default CSRF implementation, which mimics the behavior from older + versions of Pyramid. The ``new_csrf_token`` and ``get_csrf_token`` methods + are indirected to the underlying session implementation. + + Note that using this CSRF implementation requires that + a :term:`session factory` is configured. + + .. versionadded :: 1.8a1 + """ + def new_csrf_token(self, request): + """ Sets a new CSRF token into the session and returns it. """ + return request.session.new_csrf_token() + + def get_csrf_token(self, request): + """ Returns the currently active CSRF token from the session, generating + a new one if needed.""" + return request.session.get_csrf_token() + + def check_csrf_token(self, request, supplied_token): + """ Returns True if supplied_token is the same value as get_csrf_token + returns for this request. """ + expected = self.get_csrf_token(request) + return not strings_differ( + bytes_(expected, 'ascii'), + bytes_(supplied_token, 'ascii'), + ) + +@implementer(ICSRFPolicy) +class CookieCSRF(object): + """ An alternative CSRF implementation that stores its information in + unauthenticated cookies, known as the 'Double Submit Cookie' method in the + OWASP CSRF guidelines. This gives some additional flexibility with regards + to scalingas the tokens can be generated and verified by a front-end server. + + .. versionadded :: 1.8a1 + """ + + def __init__(self, cookie_name='csrf_token', secure=False, httponly=False, + domain=None, path='/'): + self.cookie_name = cookie_name + self.secure = secure + self.httponly = httponly + self.domain = domain + self.path = path + + def new_csrf_token(self, request): + """ Sets a new CSRF token into the request and returns it. """ + token = uuid.uuid4().hex + def set_cookie(request, response): + response.set_cookie( + self.cookie_name, + token, + httponly=self.httponly, + secure=self.secure, + domain=self.domain, + path=self.path, + overwrite=True, + ) + request.add_response_callback(set_cookie) + return token + + def get_csrf_token(self, request): + """ Returns the currently active CSRF token by checking the cookies + sent with the current request.""" + token = request.cookies.get(self.cookie_name) + if not token: + token = self.new_csrf_token(request) + return token + + def check_csrf_token(self, request, supplied_token): + """ Returns True if supplied_token is the same value as get_csrf_token + returns for this request. """ + expected = self.get_csrf_token(request) + return not strings_differ( + bytes_(expected, 'ascii'), + bytes_(supplied_token, 'ascii'), + ) + + +def csrf_token_template_global(event): + request = event.get('request', None) + try: + registry = request.registry + except AttributeError: + return + else: + csrf = registry.getUtility(ICSRFPolicy) + if csrf is not None: + event['get_csrf_token'] = partial(csrf.get_csrf_token, request) + + +def get_csrf_token(request): + """ Get the currently active CSRF token for the request passed, generating + a new one using ``new_csrf_token(request)`` if one does not exist. This + calls the equivalent method in the chosen CSRF protection implementation. + + .. versionadded :: 1.8a1 + """ + registry = request.registry + csrf = registry.getUtility(ICSRFPolicy) + if csrf is not None: + return csrf.get_csrf_token(request) + + +def new_csrf_token(request): + """ Generate a new CSRF token for the request passed and persist it in an + implementation defined manner. This calls the equivalent method in the + chosen CSRF protection implementation. + + .. versionadded :: 1.8a1 + """ + registry = request.registry + csrf = registry.getUtility(ICSRFPolicy) + if csrf is not None: + return csrf.new_csrf_token(request) + + +def check_csrf_token(request, + token='csrf_token', + header='X-CSRF-Token', + raises=True): + """ Check the CSRF token returned by the :meth:`pyramid.interfaces.ICSRFPolicy` + implementation against the value in ``request.POST.get(token)`` (if a POST + request) or ``request.headers.get(header)``. If a ``token`` keyword is not + supplied to this function, the string ``csrf_token`` will be used to look + up the token in ``request.POST``. If a ``header`` keyword is not supplied + to this function, the string ``X-CSRF-Token`` will be used to look up the + token in ``request.headers``. + + If the value supplied by post or by header doesn't match the value supplied + by ``impl.get_csrf_token()`` (where ``impl`` is an implementation of + :meth:`pyramid.interfaces.ICSRFPolicy`), and ``raises`` is ``True``, this + function will raise an :exc:`pyramid.exceptions.BadCSRFToken` exception. If + the values differ and ``raises`` is ``False``, this function will return + ``False``. If the CSRF check is successful, this function will return + ``True`` unconditionally. + + See :ref:`auto_csrf_checking` for information about how to secure your + application automatically against CSRF attacks. + + .. versionadded:: 1.4a2 + + .. versionchanged:: 1.7a1 + A CSRF token passed in the query string of the request is no longer + considered valid. It must be passed in either the request body or + a header. + + .. versionchanged:: 1.8a1 + Moved from pyramid.session to pyramid.csrf + """ + supplied_token = "" + # We first check the headers for a csrf token, as that is significantly + # cheaper than checking the POST body + if header is not None: + supplied_token = request.headers.get(header, "") + + # 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. + if supplied_token == "" and token is not None: + supplied_token = request.POST.get(token, "") + + policy = request.registry.getUtility(ICSRFPolicy) + if not policy.check_csrf_token(request, supplied_token): + if raises: + raise BadCSRFToken('check_csrf_token(): Invalid token') + return False + return True + + +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 + not. + + If the value supplied by the Origin or Referer header isn't one of the + trusted origins and ``raises`` is ``True``, this function will raise a + :exc:`pyramid.exceptions.BadCSRFOrigin` exception but if ``raises`` is + ``False`` this function will return ``False`` instead. If the CSRF origin + checks are successful this function will return ``True`` unconditionally. + + Additional trusted origins may be added by passing a list of domain (and + ports if nonstandard like `['example.com', 'dev.example.com:8080']`) in + with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None`` + (the default) this list of additional domains will be pulled from the + ``pyramid.csrf_trusted_origins`` setting. + + Note that this function will do nothing if request.scheme is not https. + + .. versionadded:: 1.7 + + .. versionchanged:: 1.8a1 + Moved from pyramid.session to pyramid.csrf + """ + def _fail(reason): + if raises: + raise BadCSRFOrigin(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 + + # Fail if we were not able to locate an origin at all + if not origin: + return _fail("Origin checking failed - no Origin or Referer.") + + # Parse our origin so we we can extract the required information from + # it. + originp = urlparse.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)) + 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 True diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index bbb4754e4..f58ee8b58 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -981,19 +981,30 @@ class ISession(IDict): :meth:`pyramid.interfaces.ISession.flash` """ - def new_csrf_token(): - """ Create and set into the session a new, random cross-site request - forgery protection token. Return the token. It will be a string.""" - def get_csrf_token(): - """ Return a random cross-site request forgery protection token. It - will be a string. If a token was previously added to the session via +class ICSRFPolicy(Interface): + """ An object that offers the ability to verify CSRF tokens and generate + new ones""" + + def new_csrf_token(request): + """ Create and return a new, random cross-site request forgery + protection token. Return the token. It will be a string.""" + + def get_csrf_token(request): + """ Return a cross-site request forgery protection token. It + will be a string. If a token was previously set for this user via ``new_csrf_token``, that token will be returned. If no CSRF token - was previously set into the session, ``new_csrf_token`` will be - called, which will create and set a token, and this token will be - returned. + was previously set, ``new_csrf_token`` will be called, which will + create and set a token, and this token will be returned. """ + def check_csrf_token(request, supplied_token): + """ Returns a boolean that represents if supplied_token is a valid CSRF + token for this request. Comparing strings for equality must be done + using :func:`pyramid.utils.strings_differ` to avoid timing attacks. + """ + + class IIntrospector(Interface): def get(category_name, discriminator, default=None): """ Get the IIntrospectable related to the category_name and the diff --git a/pyramid/predicates.py b/pyramid/predicates.py index 7c3a778ca..3d7bb1b4b 100644 --- a/pyramid/predicates.py +++ b/pyramid/predicates.py @@ -4,7 +4,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.compat import is_nonstr_iter -from pyramid.session import check_csrf_token +from pyramid.csrf import check_csrf_token from pyramid.traversal import ( find_interface, traversal_path, diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 47705d5d9..7d667ba7b 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -447,7 +447,6 @@ class RendererHelper(object): registry = self.registry registry.notify(system_values) - result = renderer(value, system_values) return result diff --git a/pyramid/session.py b/pyramid/session.py index 47b80f617..b1ad25410 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -16,19 +16,11 @@ from pyramid.compat import ( text_, bytes_, native_, - urlparse, ) -from pyramid.exceptions import ( - BadCSRFOrigin, - BadCSRFToken, -) from pyramid.interfaces import ISession -from pyramid.settings import aslist -from pyramid.util import ( - is_same_domain, - strings_differ, -) +from pyramid.util import strings_differ + def manage_accessed(wrapped): """ Decorator which causes a cookie to be renewed when an accessor @@ -109,149 +101,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 - not. - - If the value supplied by the Origin or Referer header isn't one of the - trusted origins and ``raises`` is ``True``, this function will raise a - :exc:`pyramid.exceptions.BadCSRFOrigin` exception but if ``raises`` is - ``False`` this function will return ``False`` instead. If the CSRF origin - checks are successful this function will return ``True`` unconditionally. - - Additional trusted origins may be added by passing a list of domain (and - ports if nonstandard like `['example.com', 'dev.example.com:8080']`) in - with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None`` - (the default) this list of additional domains will be pulled from the - ``pyramid.csrf_trusted_origins`` setting. - - Note that this function will do nothing if request.scheme is not https. - - .. versionadded:: 1.7 - """ - def _fail(reason): - if raises: - raise BadCSRFOrigin(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 - - # Fail if we were not able to locate an origin at all - if not origin: - return _fail("Origin checking failed - no Origin or Referer.") - - # Parse our origin so we we can extract the required information from - # it. - originp = urlparse.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)) - 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 True - - -def check_csrf_token(request, - token='csrf_token', - header='X-CSRF-Token', - raises=True): - """ Check the CSRF token in the request's session against the value in - ``request.POST.get(token)`` (if a POST request) or - ``request.headers.get(header)``. If a ``token`` keyword is not supplied to - this function, the string ``csrf_token`` will be used to look up the token - in ``request.POST``. If a ``header`` keyword is not supplied to this - function, the string ``X-CSRF-Token`` will be used to look up the token in - ``request.headers``. - - If the value supplied by post or by header doesn't match the value - supplied by ``request.session.get_csrf_token()``, and ``raises`` is - ``True``, this function will raise an - :exc:`pyramid.exceptions.BadCSRFToken` exception. - If the values differ and ``raises`` is ``False``, this function will - return ``False``. If the CSRF check is successful, this function will - return ``True`` unconditionally. - - Note that using this function requires that a :term:`session factory` is - configured. - - See :ref:`auto_csrf_checking` for information about how to secure your - application automatically against CSRF attacks. - - .. versionadded:: 1.4a2 - - .. versionchanged:: 1.7a1 - A CSRF token passed in the query string of the request is no longer - 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. - 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 == "" and header is not None: - supplied_token = request.headers.get(header, "") - - expected_token = request.session.get_csrf_token() - if strings_differ(bytes_(expected_token), bytes_(supplied_token)): - if raises: - raise BadCSRFToken('check_csrf_token(): Invalid token') - return False - return True class PickleSerializer(object): """ A serializer that uses the pickle protocol to dump Python diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 211632730..45495f1fa 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2373,7 +2373,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): view = lambda r: 'OK' config.set_default_csrf_options(require_csrf=True) config.add_view(view, context=Exception, renderer=null_renderer) - view_intr = introspector.introspectables[1] + view_intr = introspector.introspectables[-1] self.assertTrue(view_intr.type_name, 'view') self.assertEqual(view_intr['callable'], view) derived_view = view_intr['derived_callable'] diff --git a/pyramid/tests/test_csrf.py b/pyramid/tests/test_csrf.py new file mode 100644 index 000000000..a74d2a07b --- /dev/null +++ b/pyramid/tests/test_csrf.py @@ -0,0 +1,172 @@ +import unittest + +from pyramid.config import Configurator +from pyramid.csrf import CookieCSRF, SessionCSRF, get_csrf_token, new_csrf_token +from pyramid.events import BeforeRender +from pyramid.interfaces import ICSRFPolicy +from pyramid.tests.test_view import BaseTest as ViewBaseTest + + +class CSRFTokenTests(ViewBaseTest, unittest.TestCase): + class DummyCSRF(object): + def new_csrf_token(self, request): + return 'e5e9e30a08b34ff9842ff7d2b958c14b' + + def get_csrf_token(self, request): + return '02821185e4c94269bdc38e6eeae0a2f8' + + def test_csrf_token_passed_to_template(self): + config = Configurator() + config.set_default_csrf_options(implementation=self.DummyCSRF) + config.commit() + + request = self._makeRequest() + request.registry = config.registry + + before = BeforeRender({'request': request}, {}) + config.registry.notify(before) + self.assertIn('get_csrf_token', before) + self.assertEqual( + before['get_csrf_token'](), + '02821185e4c94269bdc38e6eeae0a2f8' + ) + + def test_simple_api_for_tokens_from_python(self): + config = Configurator() + config.set_default_csrf_options(implementation=self.DummyCSRF) + config.commit() + + request = self._makeRequest() + request.registry = config.registry + self.assertEqual( + get_csrf_token(request), + '02821185e4c94269bdc38e6eeae0a2f8' + ) + self.assertEqual( + new_csrf_token(request), + 'e5e9e30a08b34ff9842ff7d2b958c14b' + ) + + +class SessionCSRFTests(unittest.TestCase): + class MockSession(object): + def new_csrf_token(self): + return 'e5e9e30a08b34ff9842ff7d2b958c14b' + + def get_csrf_token(self): + return '02821185e4c94269bdc38e6eeae0a2f8' + + def test_session_csrf_implementation_delegates_to_session(self): + config = Configurator() + config.set_default_csrf_options(implementation=SessionCSRF) + config.commit() + + request = DummyRequest(config.registry, session=self.MockSession()) + self.assertEqual( + config.registry.getUtility(ICSRFPolicy).get_csrf_token(request), + '02821185e4c94269bdc38e6eeae0a2f8' + ) + self.assertEqual( + config.registry.getUtility(ICSRFPolicy).new_csrf_token(request), + 'e5e9e30a08b34ff9842ff7d2b958c14b' + ) + + +class CookieCSRFTests(unittest.TestCase): + + def test_get_cookie_csrf_with_no_existing_cookie_sets_cookies(self): + config = Configurator() + config.set_default_csrf_options(implementation=CookieCSRF()) + config.commit() + + response = MockResponse() + request = DummyRequest(config.registry, response=response) + + token = config.registry.getUtility(ICSRFPolicy).get_csrf_token(request) + self.assertEqual( + response.called_args, + ('csrf_token', token), + ) + self.assertEqual( + response.called_kwargs, + { + 'secure': False, + 'httponly': False, + 'domain': None, + 'path': '/', + 'overwrite': True + } + ) + + def test_existing_cookie_csrf_does_not_set_cookie(self): + config = Configurator() + config.set_default_csrf_options(implementation=CookieCSRF()) + config.commit() + + response = MockResponse() + request = DummyRequest(config.registry, response=response) + request.cookies = {'csrf_token': 'e6f325fee5974f3da4315a8ccf4513d2'} + + token = config.registry.getUtility(ICSRFPolicy).get_csrf_token(request) + self.assertEqual( + token, + 'e6f325fee5974f3da4315a8ccf4513d2' + ) + self.assertEqual( + response.called_args, + (), + ) + self.assertEqual( + response.called_kwargs, + {} + ) + + def test_new_cookie_csrf_with_existing_cookie_sets_cookies(self): + config = Configurator() + config.set_default_csrf_options(implementation=CookieCSRF()) + config.commit() + + response = MockResponse() + request = DummyRequest(config.registry, response=response) + request.cookies = {'csrf_token': 'e6f325fee5974f3da4315a8ccf4513d2'} + + token = config.registry.getUtility(ICSRFPolicy).new_csrf_token(request) + self.assertEqual( + response.called_args, + ('csrf_token', token), + ) + self.assertEqual( + response.called_kwargs, + { + 'secure': False, + 'httponly': False, + 'domain': None, + 'path': '/', + 'overwrite': True + } + ) + + +class DummyRequest(object): + registry = None + session = None + cookies = {} + + def __init__(self, registry, session=None, response=None): + self.registry = registry + self.session = session + self.response = response + + def add_response_callback(self, callback): + callback(self, self.response) + + +class MockResponse(object): + def __init__(self): + self.called_args = () + self.called_kwargs = {} + + def set_cookie(self, *args, **kwargs): + self.called_args = args + self.called_kwargs = kwargs + return diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 3a308d08b..b51dccecc 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -661,7 +661,7 @@ class Test_signed_deserialize(unittest.TestCase): class Test_check_csrf_token(unittest.TestCase): def _callFUT(self, *args, **kwargs): - from ..session import check_csrf_token + from ..csrf import check_csrf_token return check_csrf_token(*args, **kwargs) def test_success_token(self): @@ -709,7 +709,7 @@ class Test_check_csrf_token(unittest.TestCase): class Test_check_csrf_origin(unittest.TestCase): def _callFUT(self, *args, **kwargs): - from ..session import check_csrf_origin + from ..csrf import check_csrf_origin return check_csrf_origin(*args, **kwargs) def test_success_with_http(self): diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 4eb0ce704..d2869b162 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -6,7 +6,7 @@ from zope.interface import ( ) from pyramid.security import NO_PERMISSION_REQUIRED -from pyramid.session import ( +from pyramid.csrf import ( check_csrf_origin, check_csrf_token, ) |
