summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthew Wilkes <git@matthewwilkes.name>2016-12-05 12:16:26 +0100
committerMatthew Wilkes <git@matthewwilkes.name>2017-04-12 12:13:45 +0100
commita2c7c7a49bceeaaab2853e7e73c3671979d4c9ed (patch)
treebdc3640fad15bc3ea257df26399ff8aaaee14bd1
parent387993115ee777784654c95d9f2f8d8ce7c4f5e4 (diff)
downloadpyramid-a2c7c7a49bceeaaab2853e7e73c3671979d4c9ed.tar.gz
pyramid-a2c7c7a49bceeaaab2853e7e73c3671979d4c9ed.tar.bz2
pyramid-a2c7c7a49bceeaaab2853e7e73c3671979d4c9ed.zip
Create a new ICSRF implementation for getting CSRF tokens, split out from the session machinery.
Adds configuration of this to the csrf_options configurator commands. Make the default implementation a fallback to the old one. Documentation patches for new best practices given updates CSRF implementation.
-rw-r--r--CHANGES.txt12
-rw-r--r--docs/api/csrf.rst18
-rw-r--r--docs/api/interfaces.rst3
-rw-r--r--docs/api/session.rst4
-rw-r--r--docs/narr/security.rst191
-rw-r--r--docs/narr/sessions.rst175
-rw-r--r--pyramid/config/security.py16
-rw-r--r--pyramid/config/views.py14
-rw-r--r--pyramid/csrf.py286
-rw-r--r--pyramid/interfaces.py29
-rw-r--r--pyramid/predicates.py2
-rw-r--r--pyramid/renderers.py1
-rw-r--r--pyramid/session.py155
-rw-r--r--pyramid/tests/test_config/test_views.py2
-rw-r--r--pyramid/tests/test_csrf.py172
-rw-r--r--pyramid/tests/test_session.py4
-rw-r--r--pyramid/viewderivers.py2
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,
)