Skip to content

Commit

Permalink
Use the webob CookieProfile in the Cookie implementation, rename some…
Browse files Browse the repository at this point in the history
… implemenations based on feedback, split CSRF implementation and option configuration and make the csrf token function exposed as a system default rather than a renderer event.
  • Loading branch information
MatthewWilkes committed Apr 12, 2017
1 parent f6d63a4 commit 7c0f098
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 152 deletions.
1 change: 1 addition & 0 deletions docs/api/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
.. automethod:: set_authentication_policy
.. automethod:: set_authorization_policy
.. automethod:: set_default_csrf_options
.. automethod:: set_csrf_storage_policy
.. automethod:: set_default_permission
.. automethod:: add_permission

Expand Down
4 changes: 2 additions & 2 deletions docs/api/csrf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

.. automodule:: pyramid.csrf

.. autoclass:: SessionCSRF
.. autoclass:: SessionCSRFStoragePolicy
:members:

.. autoclass:: CookieCSRF
.. autoclass:: CookieCSRFStoragePolicy
:members:

.. autofunction:: get_csrf_token
Expand Down
1 change: 1 addition & 0 deletions docs/narr/extconfig.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ Pre-defined Phases
- :meth:`pyramid.config.Configurator.override_asset`
- :meth:`pyramid.config.Configurator.set_authorization_policy`
- :meth:`pyramid.config.Configurator.set_default_csrf_options`
- :meth:`pyramid.config.Configurator.set_csrf_storage_policy`
- :meth:`pyramid.config.Configurator.set_default_permission`
- :meth:`pyramid.config.Configurator.set_view_mapper`

Expand Down
8 changes: 4 additions & 4 deletions docs/narr/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -780,15 +780,15 @@ and then requiring that it be present in all potentially unsafe requests.
:app:`Pyramid` provides facilities to create and check CSRF tokens.

By default :app:`Pyramid` comes with a session-based CSRF implementation
:class:`pyramid.csrf.SessionCSRF`. To use it, you must first enable
:class:`pyramid.csrf.SessionCSRFStoragePolicy`. To use it, you must first enable
a :term:`session factory` as described in
:ref:`using_the_default_session_factory` or
:ref:`using_alternate_session_factories`. Alternatively, you can use
a cookie-based implementation :class:`pyramid.csrf.CookieCSRF` which gives
a cookie-based implementation :class:`pyramid.csrf.CookieCSRFStoragePolicy` which gives
some additional flexibility as it does not require a session for each user.
You can also define your own implementation of
:class:`pyramid.interfaces.ICSRFStoragePolicy` and register it with the
:meth:`pyramid.config.Configurator.set_default_csrf_options` directive.
:meth:`pyramid.config.Configurator.set_csrf_storage_policy` directive.

For example:

Expand All @@ -797,7 +797,7 @@ For example:
from pyramid.config import Configurator
config = Configurator()
config.set_default_csrf_options(implementation=MyCustomCSRFPolicy())
config.set_csrf_storage_policy(MyCustomCSRFPolicy())
.. index::
single: csrf.get_csrf_token
Expand Down
1 change: 1 addition & 0 deletions pyramid/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ def setup_registry(self,
self.add_default_view_derivers()
self.add_default_route_predicates()
self.add_default_tweens()
self.add_default_security()

if exceptionresponse_view is not None:
exceptionresponse_view = self.maybe_dotted(exceptionresponse_view)
Expand Down
31 changes: 19 additions & 12 deletions pyramid/config/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
PHASE2_CONFIG,
)

from pyramid.csrf import csrf_token_template_global
from pyramid.csrf import SessionCSRF
from pyramid.events import BeforeRender
from pyramid.csrf import SessionCSRFStoragePolicy
from pyramid.exceptions import ConfigurationError
from pyramid.util import action_method
from pyramid.util import as_sorted_tuple


class SecurityConfiguratorMixin(object):

def add_default_security(self):
self.set_csrf_storage_policy(SessionCSRFStoragePolicy())

@action_method
def set_authentication_policy(self, policy):
""" Override the :app:`Pyramid` :term:`authentication policy` in the
Expand Down Expand Up @@ -170,7 +172,6 @@ def add_permission(self, permission_name):
@action_method
def set_default_csrf_options(
self,
implementation=None,
require_csrf=True,
token='csrf_token',
header='X-CSRF-Token',
Expand All @@ -180,10 +181,6 @@ def set_default_csrf_options(
"""
Set the default CSRF options used by subsequent view registrations.
``implementation`` is a class that implements the
:meth:`pyramid.interfaces.ICSRFStoragePolicy` 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
Expand Down Expand Up @@ -217,10 +214,7 @@ def set_default_csrf_options(
options = DefaultCSRFOptions(
require_csrf, token, header, safe_methods, callback,
)
if implementation is None:
implementation = SessionCSRF()
def register():
self.registry.registerUtility(implementation, ICSRFStoragePolicy)
self.registry.registerUtility(options, IDefaultCSRFOptions)
intr = self.introspectable('default csrf view options',
None,
Expand All @@ -232,10 +226,23 @@ def register():
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,))

@action_method
def set_csrf_storage_policy(self, policy):
"""
Set the CSRF storage policy used by subsequent view registrations.
``policy`` is a class that implements the
:meth:`pyramid.interfaces.ICSRFStoragePolicy` interface that will be used for all
CSRF functionality.
"""
def register():
self.registry.registerUtility(policy, ICSRFStoragePolicy)

self.action(ICSRFStoragePolicy, register, order=PHASE1_CONFIG)


@implementer(IDefaultCSRFOptions)
class DefaultCSRFOptions(object):
Expand Down
52 changes: 22 additions & 30 deletions pyramid/csrf.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from functools import partial
import uuid

from webob.cookies import CookieProfile
from zope.interface import implementer


from pyramid.authentication import _SimpleSerializer

from pyramid.compat import (
urlparse,
bytes_
Expand All @@ -20,7 +23,7 @@


@implementer(ICSRFStoragePolicy)
class SessionCSRF(object):
class SessionCSRFStoragePolicy(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.
Expand Down Expand Up @@ -49,7 +52,7 @@ def check_csrf_token(self, request, supplied_token):
)

@implementer(ICSRFStoragePolicy)
class CookieCSRF(object):
class CookieCSRFStoragePolicy(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
Expand All @@ -60,33 +63,34 @@ class CookieCSRF(object):
"""

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
domain=None, max_age=None, path='/'):
serializer = _SimpleSerializer()
self.cookie_profile = CookieProfile(
cookie_name=cookie_name,
secure=secure,
max_age=max_age,
httponly=httponly,
path=path,
serializer=serializer
)
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,
self.cookie_profile.set_cookies(
response,
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)
bound_cookies = self.cookie_profile.bind(request)
token = bound_cookies.get_value()
if not token:
token = self.new_csrf_token(request)
return token
Expand All @@ -100,18 +104,6 @@ def check_csrf_token(self, request, supplied_token):
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(ICSRFStoragePolicy)
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
Expand Down Expand Up @@ -188,9 +180,9 @@ def check_csrf_token(request,
if policy is None:
# There is no policy set, but we are trying to validate a CSRF token
# This means explicit validation has been asked for without configuring
# the CSRF implementation. Fall back to SessionCSRF as that is the
# the CSRF implementation. Fall back to SessionCSRFStoragePolicy as that is the
# default
policy = SessionCSRF()
policy = SessionCSRFStoragePolicy()
if not policy.check_csrf_token(request, supplied_token):
if raises:
raise BadCSRFToken('check_csrf_token(): Invalid token')
Expand Down
4 changes: 4 additions & 0 deletions pyramid/renderers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from functools import partial
import json
import os
import re
Expand All @@ -19,6 +20,7 @@
text_type,
)

from pyramid.csrf import get_csrf_token
from pyramid.decorator import reify

from pyramid.events import BeforeRender
Expand Down Expand Up @@ -428,6 +430,7 @@ def render_view(self, request, response, view, context):
'context':context,
'request':request,
'req':request,
'get_csrf_token':partial(get_csrf_token, request),
}
return self.render_to_response(response, system, request=request)

Expand All @@ -441,6 +444,7 @@ def render(self, value, system_values, request=None):
'context':getattr(request, 'context', None),
'request':request,
'req':request,
'get_csrf_token':partial(get_csrf_token, request),
}

system_values = BeforeRender(system_values, value)
Expand Down
Loading

0 comments on commit 7c0f098

Please sign in to comment.