Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple CSRF protection from the session machinery #2854

Merged
merged 8 commits into from
Apr 30, 2017

Conversation

MatthewWilkes
Copy link
Contributor

In #1573 @dstufft makes a number of feature requests about the CSRF implementation of Pyramid, most of which were added in 1.7.

This patch attempts to move the CSRF generation logic out of the session, so new CSRF implementations that do not depend on sessions can be added. It currently does not add any such implementations, the only one shipped is the default which falls back to an implementation of CSRF on the session.

It also introduces new utility functions for get_csrf_token(request) and new_csrf_token(request) in Python, as well as a get_csrf_token() in the template global scope, and repoints documentation to use these rather than explicitly calling the session machinery.

This was written at the DragonSprint by me and @jcerjak, a new contributor to Pyramid.

pyramid/csrf.py Outdated
csrf = registry.queryUtility(ICSRF)
if csrf is not None:
event['get_csrf_token'] = partial(csrf.get_csrf_token, request)
except AttributeError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this except needed since request.registry is not always available? It might mask an error if csrf does not implement get_csrf_token (not likely, but still).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also catches the case when request is None or some weird mock object, like in tests. I've moved the except handler only to cover request.registry and the rest in in an else handler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The utility promises to implement ICSRF, which guarantees that it has a get_csrf_token method. We don't need to defend against borked implementations.

@@ -218,9 +227,14 @@ def register():
intr['header'] = header
intr['safe_methods'] = as_sorted_tuple(safe_methods)
intr['callback'] = callback

from pyramid.csrf import csrf_token_template_global
from pyramid.events import BeforeRender
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move these imports to the top?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

pyramid/csrf.py Outdated
)
from pyramid.interfaces import ICSRF
from pyramid.settings import aslist
from zope.interface import implementer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third-party imports should be prior to "local" imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, moving.

pyramid/csrf.py Outdated
csrf = registry.queryUtility(ICSRF)
if csrf is not None:
event['get_csrf_token'] = partial(csrf.get_csrf_token, request)
except AttributeError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The utility promises to implement ICSRF, which guarantees that it has a get_csrf_token method. We don't need to defend against borked implementations.


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`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be language here which points up the fact that enabling sessions is a requirement of the default ICSRF implementation, rather than a general requirement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe @jcerjak is looking at rewording this, is that the case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed it in the code docstrings, but missed it here, fixing.

token = get_csrf_token(request)

The ``get_csrf_token()`` method accepts a single argument, of the request. It
returns a CSRF *token* string. If ``get_csrf_token()`` or ``new_csrf_token()``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accepts a single argument, of the request.

The "of" should not be there.


.. code-block:: javascript

var csrfToken = ${get_csrf_token()};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My JS-fu may be weak, but ISTM that the token value needs to be wrapped in quotes here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the same is true of the current documentation, I didn't pick up on it when I changed request.session.get_csrf_token to get_csrf_token.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use .. note::.

pyramid/csrf.py Outdated
registry = request.registry
csrf = registry.queryUtility(ICSRF)
if csrf is not None:
return csrf.get_csrf_token(request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this helper raise if no utility is registered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, I think you can make arguments either way. If you've written third part code that's intended to be reusable it means you have to special case when users haven't got CSRF enabled.

Either way, we should document the behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If third-party code registers a view with require_csrf, AFAIK there is no way straightforward way to disable CSRF protection.

As discussed with @MatthewWilkes , if we want to support this use case, we could add a CSRF option for disabling the protection. Or a user could register a dummy ICSRF implementation, which just returns an empty string. But in any case we should raise an error if no ICSRF utility is registered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If third-party code registers a view with require_csrf, AFAIK there is no way straightforward way to disable CSRF protection.

This is intentional... if someone defines a view that explicitly requires csrf then it will be inaccessible until someone registers a csrf policy for the app. Turning off csrf protection for those views is a non-goal and can already be done just by re-defining it after config.commit() to override the old view.

I'd expect this code to raise if there is no policy. Simply using getUtility should be enough.

pyramid/csrf.py Outdated
registry = request.registry
csrf = registry.queryUtility(ICSRF)
if csrf is not None:
return csrf.new_csrf_token(request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this helper raise if no utility is registered?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should raise. Using getUtility should be enough.

pyramid/csrf.py Outdated
if supplied_token == "" and header is not None:
supplied_token = request.headers.get(header, "")

impl = request.registry.getUtility(ICSRF)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this helper raise if no utility is registered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this one is getUtility it will raise a ComponentLookupError if it's missing, were you thinking of something a bit more descriptive?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my eyes read that as queryUtility() like the ones above.

Copy link
Member

@mmerickel mmerickel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is on the right track, great work so far!

Could we consider adding an alternate implementation that does not store the CSRF token in the session to prove the viability of this approach? After all, there's no point to this feature if it doesn't change that status quo.

Maybe just a simple CookieCSRF is enough? AFAIK it does not need to be signed, but @bertjwregeer will be happy to correct my ignorance if I'm wrong.

Finally I'd like to think of a better name for the interface than ICSRF such as ICSRFTokenStorage or something. It's a bit weird because the interface itself doesn't control the rest of the default options. Similarly naming it impl and implementation in the code is a bit too generic for me versus storage or policy or something. I'm not quite sure yet.

@@ -0,0 +1,40 @@
What's New in Pyramid 1.8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't add a whatsnew document. We will do it prior to release. Your changes are good I just think it's easier to create that file from scratch just before a major release.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks!

pyramid/csrf.py Outdated
registry = request.registry
csrf = registry.queryUtility(ICSRF)
if csrf is not None:
return csrf.get_csrf_token(request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If third-party code registers a view with require_csrf, AFAIK there is no way straightforward way to disable CSRF protection.

This is intentional... if someone defines a view that explicitly requires csrf then it will be inaccessible until someone registers a csrf policy for the app. Turning off csrf protection for those views is a non-goal and can already be done just by re-defining it after config.commit() to override the old view.

I'd expect this code to raise if there is no policy. Simply using getUtility should be enough.

pyramid/csrf.py Outdated
registry = request.registry
csrf = registry.queryUtility(ICSRF)
if csrf is not None:
return csrf.new_csrf_token(request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should raise. Using getUtility should be enough.

pyramid/csrf.py Outdated
registry = request.registry
csrf = registry.queryUtility(ICSRF)
if csrf is not None:
return csrf.get_csrf_token(request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be functions or should we add them to the request as methods, similar to request.has_permission, request.session.get_csrf_token etc? request.get_csrf_token()? request.csrf.get_token()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that new_csrf_token and get_csrf_token should be available in the same place, but I'm a little nervous about having new_csrf_token available in templates, personally. Maybe I'm worrying over nothing, but it feels to me like something that should only be called in Python code.

@digitalresistor
Copy link
Member

The CSRF cookie doesn't need to be signed.

pyramid/csrf.py Outdated
# 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend swapping the check for the token in the headers/body. request.POST is more expensive than checking request.headers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unchanged from the previous implementation, but that makes sense. Switching.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @MatthewWilkes!

@MatthewWilkes
Copy link
Contributor Author

@mmerickel I've pondered a bit about the change to be specifically about storage, and think that it might be worthwhile to have some of the checking in the CSRF implementation too. Specifically, currently it assumes that constant-time string comparison is sufficient, but an implementation might want to do funky stuff like scope tokens (as mentioned in the original ticket).

If we have a hook there then we can allow this to be supported without having to also supply a callback.

@MatthewWilkes
Copy link
Contributor Author

@mmerickel @jcerjak and I worked on this more today and incorporated changes based on your feedback. We have added the checking function I referenced in my previous comment, as I think it gives us a bit more flexibility.

Copy link
Member

@mmerickel mmerickel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I like ICSRFPolicy because it's too similar to the current default_csrf_options which I almost named and defined as a policy when I was doing that work, so I think we should avoid that word.

How would scope tokens be done with this new API? I think it's important to at least consider and I don't have a good answer for it right now.

How does this feature influence user apps right now? Does the session factory automatically register a csrf implementation somehow? Or does pyramid's configurator just always enable the SessionCSRF by default, similar to default renderers?

pyramid/csrf.py Outdated
return
else:
csrf = registry.getUtility(ICSRFPolicy)
event['get_csrf_token'] = partial(csrf.get_csrf_token, request)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is code from my own apps that I'll at least paste here incase you like it better.

def includeme(config):
    # other session setup usually
    # ...
    config.add_subscriber(renderer_globals, 'pyramid.interfaces.IBeforeRender')

class CSRFToken(object):
    def __init__(self, request):
        self.request = request

    def __json__(self, request):
        return self.request.session.get_csrf_token()

    def __html__(self):
        return self.request.session.get_csrf_token()

def renderer_globals(event):
    request = event['request']
    event['csrf_token'] = CSRFToken(request)  # allows {{ csrf_token }}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I like this idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this feels like a little too much magic personally. I can certainly see the simplicity benefits, but the current approach has a couple of minor benefits too:

  1. A function call feels more expensive, which considering setting a CSRF cookie can cause pages to not be trivially cachable may theoretically prevent people adding it 'just in case'
  2. It is closer to the existing idiom, users would change request.session.get_csrf_token() to get_csrf_token(), which may make upgrades easier.

For what it's worth, I don't think either of those two arguments are sufficient to make it the only feasible option, I just thought I'd share my thinking.

I don't think I want to integrate this change at this stage, maybe it would be worth another PR? What do you think @jcerjak?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the simplicity, but maybe it's better to be more explicit here. Don't have a strong opinion about it, but I think it would be better if it's a separate PR.

@MatthewWilkes
Copy link
Contributor Author

Will reply to the rest tomorrow, but if you can give us steer on naming that would help. Do you prefer ICSRFStorage?

@mmerickel
Copy link
Member

Will reply to the rest tomorrow, but if you can give us steer on naming that would help. Do you prefer ICSRFStorage?

Yeah sorry, I know naming is the biggest derailment. Let's go with ICSRFStoragePolicy to disambiguate it from the rest of the csrf settings.

@MatthewWilkes
Copy link
Contributor Author

MatthewWilkes commented Dec 9, 2016

@mmerickel With regards to scoped tokens, this isn't included in core (and I personally don't think it should be), but it's nice and easy to implement in userspace with these changes. The implementation would be something like:

from functools import wraps
import hmac

from zope.interface import implementer

from pyramid.csrf import ICSRFStoragePolicy, SessionCSRF
from pyramid.compat import bytes_

def csrf_scope(scope):
    def csrf_scope_decorator(func):
        @wraps(func)
        def with_scope(request, *args, **kwargs):
            request.csrf_scope = scope
            return func(request, *args, **kwargs)
        return with_scope
    return csrf_scope_decorator

@implementer(ICSRFStoragePolicy)
class ScopedSessionCSRF(SessionCSRF):

    def _get_scope(self, request):
        return getattr(request, 'csrf_scope', '')

    def _scope_token(self, scope, base_token):
        return hmac.new(bytes_(base_token, 'ascii'), scope).hexdigest()

    def new_csrf_token(self, request):
        base_token = super(ScopedSessionCSRF, self).new_csrf_token(request)
        return self._scope_token(self._get_scope(request), base_token)

    def get_csrf_token(self, request):
        base_token = super(ScopedSessionCSRF, self).get_csrf_token(request)
        return self._scope_token(self._get_scope(request), base_token)

and would be used like:

@view_config(route_name='home', renderer='templates/mytemplate.pt')
@csrf_scope('foobar')
def my_view(request):
    return {'project': 'hacking'}

@MatthewWilkes
Copy link
Contributor Author

@mmerickel WRT user facing changes, that's a good point. I've added a fallback to the check_csrf_token method that ensures that if a user does not configure CSRF the old behaviour of assuming the session will manage it is preserved.

That means that if the user explicitly marks a view as being CSRF checked without calling config.set_default_csrf_options(require_csrf=True) they will get SessionCSRF if there is a session present or an error about no session being configured if not.

Copy link
Member

@mmerickel mmerickel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking pyramid should just register a SessionCSRFStoragePolicy by default in Configurator.__init__ via calling config.set_csrf_storage_policy(SessionCSRFStoragePolicy()). in order to keep bw-compat. This would alleviate the need for the fallback policy creation in each method (check_csrf_token and new_csrf_token). If someone accesses these without a session it'll just raise which is fine.

from pyramid.config import Configurator

config = Configurator()
config.set_default_csrf_options(implementation=MyCustomCSRFPolicy())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer if this were a separate method for config.set_csrf_storage_policy as CSRF is entirely unusable without it and it would be good if it could be replaced without changing the other options.

pyramid/csrf.py Outdated


@implementer(ICSRFStoragePolicy)
class SessionCSRF(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SessionCSRFStoragePolicy

pyramid/csrf.py Outdated
)

@implementer(ICSRFStoragePolicy)
class CookieCSRF(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CookieCSRFStoragePolicy

@@ -218,9 +231,12 @@ def register():
intr['header'] = header
intr['safe_methods'] = as_sorted_tuple(safe_methods)
intr['callback'] = callback

self.add_subscriber(csrf_token_template_global, [BeforeRender])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently Pyramid has no precedent for automatically registering a BeforeRender subscriber and I think we should keep it that way. If you really want to inject it we could a) do it in the current renderer bindings (pyramid_jinja2, pyramid_mako, pyramid_chameleon) or b) do it in the pyramid.renderer.RendererHelper as a system value.

pyramid/csrf.py Outdated
self.secure = secure
self.httponly = httponly
self.domain = domain
self.path = path
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation should be using the webob.cookies.CookieProfile from webob in order to set the cookies on potentially multiple domains similar to how SignedCookieSessionFactory and AuthTktAuthenticationPolicy operate.

MatthewWilkes and others added 6 commits April 12, 2017 12:13
…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.
… review

regarding naming of variables and code cleanup.
…configure a session and set explicit checks would see an exception
… 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.
@MatthewWilkes MatthewWilkes force-pushed the csrf_session_decouple branch from a3e3f70 to 7c0f098 Compare April 12, 2017 11:14
@MatthewWilkes
Copy link
Contributor Author

@mmerickel I've looked at this again today, as I now have a client who wants CSRF protection without sessions. I've pushed changes to address your comments, let me know when you've had a chance to take a look?

Copy link
Member

@digitalresistor digitalresistor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 liking this a lot :-D

pyramid/csrf.py Outdated
considered valid. It must be passed in either the request body or
a header.

.. versionchanged:: 1.8a1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are going to need to change these to Pyramid 1.9

Copy link
Member

@mmerickel mmerickel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MatthewWilkes this is really great stuff! I think one more round with very minor tweaks and we're good to go.

@@ -320,179 +319,3 @@ flash storage.
.. index::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these index directives need to be moved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

def register():
self.registry.registerUtility(policy, ICSRFStoragePolicy)

self.action(ICSRFStoragePolicy, register, order=PHASE1_CONFIG)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure the PHASE1_CONFIG is unnecessary here. It's used in set_default_csrf_options because it needs to be configured prior to add_view running the csrf_view view deriver. I think the storage policy is only used at request-time though so can be left at the default config phase.

If you think it should be PHASE1_CONFIG then it should be added to http://docs.pylonsproject.org/projects/pyramid/en/master/narr/extconfig.html#ordering-actions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had added it to extconfig, but you're right. Removing it from here and extconfig.

pyramid/csrf.py Outdated
# This means explicit validation has been asked for without configuring
# the CSRF implementation. Fall back to SessionCSRFStoragePolicy as that is the
# default
policy = SessionCSRFStoragePolicy()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably unnecessary now. Just use getUtility instead an exception if there's no policy. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thanks. I was missing a call in the testing setup that this was hiding. Fixed.

mmerickel added a commit to mmerickel/pyramid that referenced this pull request Apr 29, 2017
- Renamed `SessionCSRFStoragePolicy` to `LegacySessionCSRFStoragePolicy` for
  the version that uses the legacy `ISession.get_csrf_token` and
  `ISession.new_csrf_token` apis and set that as the default.

- Added new `SessionCSRFStoragePolicy` that stores data in the session
  similar to how the `SessionAuthenticationPolicy` works.

- `CookieCSRFStoragePolicy` did not properly return the newly generated
  token from `get_csrf_token` after calling `new_csrf_token`. It needed
  to cache the new value since the response callback does not affect
  the current request.

- `CookieCSRFStoragePolicy` was not forwarding the `domain` value to the
  `CookieProfile` causing that setting to be ignored.

- Removed `check_csrf_token` from the `ICSRFStoragePolicy` interface
  to simplify implementations of storage policies.

- Added an introspectable item for the configured storage policy so that
  it appears on the debugtoolbar.

- Added a change note on `ISession` that it no longer required the csrf methods.
Copy link
Member

@mmerickel mmerickel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through this tonight and found several minor issues.

  • SessionCSRFStoragePolicy should really just use the session as a storage mechanism. I added LegacySessionCSRFStoragePolicy for the version that uses the legacy ISession.get_csrf_token and ISession.new_csrf_token apis and set that as the default. I also updated SessionCSRFStoragePolicy to just store data in the session similar to how the SessionAuthenticationPolicy works.

  • CookieCSRFStoragePolicy did not properly return the newly generated token from get_csrf_token after calling new_csrf_token. It needs to cache the new value since the response callback does not affect the current request.

  • CookieCSRFStoragePolicy was not forwarding the domain value to the CookieProfile causing that setting to be ignored.

  • I removed check_csrf_token from the ICSRFStoragePolicy interface because I can't see the point to making it pluggable. On top of that it makes implementation of storage policies more difficult.

  • Added an introspectable item for the configured storage policy so that it appears on the debugtoolbar.

  • Added a change note on ISession that it no longer required the csrf methods.

  • Restored (but deprecated) shims in pyramid.session for check_csrf_origin and check_csrf_token.

mmerickel added a commit to mmerickel/pyramid that referenced this pull request Apr 29, 2017
- Renamed `SessionCSRFStoragePolicy` to `LegacySessionCSRFStoragePolicy` for
  the version that uses the legacy `ISession.get_csrf_token` and
  `ISession.new_csrf_token` apis and set that as the default.

- Added new `SessionCSRFStoragePolicy` that stores data in the session
  similar to how the `SessionAuthenticationPolicy` works.

- `CookieCSRFStoragePolicy` did not properly return the newly generated
  token from `get_csrf_token` after calling `new_csrf_token`. It needed
  to cache the new value since the response callback does not affect
  the current request.

- `CookieCSRFStoragePolicy` was not forwarding the `domain` value to the
  `CookieProfile` causing that setting to be ignored.

- Removed `check_csrf_token` from the `ICSRFStoragePolicy` interface
  to simplify implementations of storage policies.

- Added an introspectable item for the configured storage policy so that
  it appears on the debugtoolbar.

- Added a change note on `ISession` that it no longer required the csrf methods.
mmerickel added a commit to mmerickel/pyramid that referenced this pull request Apr 29, 2017
- Renamed `SessionCSRFStoragePolicy` to `LegacySessionCSRFStoragePolicy` for
  the version that uses the legacy `ISession.get_csrf_token` and
  `ISession.new_csrf_token` apis and set that as the default.

- Added new `SessionCSRFStoragePolicy` that stores data in the session
  similar to how the `SessionAuthenticationPolicy` works.

- `CookieCSRFStoragePolicy` did not properly return the newly generated
  token from `get_csrf_token` after calling `new_csrf_token`. It needed
  to cache the new value since the response callback does not affect
  the current request.

- `CookieCSRFStoragePolicy` was not forwarding the `domain` value to the
  `CookieProfile` causing that setting to be ignored.

- Removed `check_csrf_token` from the `ICSRFStoragePolicy` interface
  to simplify implementations of storage policies.

- Added an introspectable item for the configured storage policy so that
  it appears on the debugtoolbar.

- Added a change note on `ISession` that it no longer required the csrf methods.

- Leave deprecated shims in ``pyramid.session`` for
``check_csrf_origin`` and ``check_csrf_token``.
@mmerickel mmerickel dismissed stevepiercy’s stale review April 29, 2017 07:09

typo is fixed (function is removed entirely) in #3019

@mmerickel
Copy link
Member

This PR will closed and show as merged automatically when #3019 is merged.

@MatthewWilkes
Copy link
Contributor Author

@mmerickel Please don't remove the check_csrf_token method, it is very useful to have it pluggable as that allows for keyring based or fully-stateless CSRF policies.

For example, consider this CSRF policy:

implementer(ICSRFStoragePolicy)
class HMACCSRFStoragePolicy(object):
    """ An alternative CSRF implementation that uses short-lived HMAC keyed on
    the user.
    """

    def __init__(self, key):
        self.key = key

    def new_csrf_token(self, request):
        user = request.authenticated_userid
        generated = int(time.time())
        digest = hmac.new(self.key, 'csrf-{}-{}'.format(user, generated), hashlib.sha1).hexdigest()
        return '{}:{}:{}'.format(user, generated, digest)

    def get_csrf_token(self, request):
        return self.new_csrf_token(request)

    def check_csrf_token(self, request, supplied_token):
        user, generated, digest = supplied_token.split(':')
        if user != request.authenticated_userid:
            return False
        if int(generated) < (time.time() - 300):
            return False
        expected = hmac.new(self.key, 'csrf-{}-{}'.format(user, generated), hashlib.sha1).hexdigest()
        return hmac.compare_digest(expected, digest)

For non-security experts reading this, I'm not claiming that the above is a good CSRF implementation, however I do think that it demonstrates that there is a value in allowing the CSRF implementation to decide what's valid and what isn't.

If you're concerned about boilerplate (although, personally, I think it's minimal), maybe we could make a mixin available?

@digitalresistor
Copy link
Member

@MatthewWilkes get_csrf_token has access to the request object. You will have to manually pull the existing token out of the appropriate header/POST data or wherever, but it is still possible to do what you want to do.

@MatthewWilkes
Copy link
Contributor Author

This is true, good point.

@mmerickel
Copy link
Member

mmerickel commented Apr 30, 2017

@MatthewWilkes thank you very much for the alternative example. I definitely had not considered this stateless use case. It appears that the main downside of supporting that use case without a pluggable check_csrf_token is that the policy is now responsible for parsing the header/body itself to find the supplied token. This feels vaguely similar to unauthenticated_userid versus authenticated_userid in authentication policies. Perhaps I can add another API to get the "unauthenticated" csrf token which can be used if necessary. I'm thinking find_csrf_token or get_unverified_csrf_token.

EDIT: I thought about it a bit longer and I can't actually provide this get_unverified_csrf_token API because it is not standardized where to find the tokens in the request. It can be different based on csrf defaults and for other reasons like the fact that check_csrf_token allows the user to specify their own key/header. I will bring back policy.check_csrf_token and try to be happy that we have the most over-engineered csrf protection in the business.

@MatthewWilkes
Copy link
Contributor Author

Thanks for coming to that decision, it occured to me this morning that Bert's workaround has a fatal flaw. Any submission that includes a CSRF check would cause get_card_token to return the supplied token, making it very easy to hit the short timeout. Imagine a form that takes 3 minutes to fill in, once you've submitted that the resultant page render will use a token with only 2 minutes of validity, so a multipage form would quickly fail.

As to being the most overengineered in the business, I have one word for you, Plone.

@mmerickel mmerickel merged commit 4b3603a into Pylons:master Apr 30, 2017
mmerickel added a commit that referenced this pull request Apr 30, 2017
Decouple CSRF protection from the session machinery (replaced #2854)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants