From ab09314ed34bc53aec64cd3998add65d0da187ad Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 14 Dec 2016 15:41:15 -0600 Subject: [PATCH] Add initial support for CSRF token protection --- tailbone/forms/__init__.py | 2 +- tailbone/forms/alchemy.py | 33 ++++++++++++++++++- tailbone/forms/core.py | 11 +++++++ tailbone/forms/simpleform.py | 8 ++++- tailbone/templates/forms/form.mako | 1 + tailbone/templates/login.mako | 1 + .../purchases/batches/receive_form.mako | 1 + tailbone/views/auth.py | 10 ++++-- 8 files changed, 61 insertions(+), 6 deletions(-) diff --git a/tailbone/forms/__init__.py b/tailbone/forms/__init__.py index e24af4c3..cbdc493e 100644 --- a/tailbone/forms/__init__.py +++ b/tailbone/forms/__init__.py @@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import from formencode import Schema -from .core import Form, Field, FieldSet, GenericFieldSet +from .core import Form, Field, FieldSet, GenericFieldSet, invalid_csrf_token from .simpleform import SimpleForm, FormRenderer from .alchemy import AlchemyForm from .fields import AssociationProxyField diff --git a/tailbone/forms/alchemy.py b/tailbone/forms/alchemy.py index 143193b0..874eecc4 100644 --- a/tailbone/forms/alchemy.py +++ b/tailbone/forms/alchemy.py @@ -30,8 +30,10 @@ from rattail.core import Object import formalchemy as fa from pyramid.renderers import render +from webhelpers.html import HTML, tags from tailbone.db import Session +from tailbone.forms import invalid_csrf_token class TemplateEngine(fa.templates.TemplateEngine): @@ -54,11 +56,12 @@ class AlchemyForm(Object): allow_successive_creates = False - def __init__(self, request, fieldset, session=None, **kwargs): + def __init__(self, request, fieldset, session=None, csrf_field='_csrf', **kwargs): super(AlchemyForm, self).__init__(**kwargs) self.request = request self.fieldset = fieldset self.session = session + self.csrf_field = csrf_field def _get_readonly(self): return self.fieldset.readonly @@ -70,6 +73,31 @@ class AlchemyForm(Object): def successive_create_label(self): return "%s and continue" % self.create_label + def csrf(self, name=None): + """ + NOTE: this method was copied from `pyramid_simpleform.FormRenderer` + + Returns the CSRF hidden input. Creates new CSRF token + if none has been assigned yet. + + The name of the hidden field is **_csrf** by default. + """ + name = name or self.csrf_field + + token = self.request.session.get_csrf_token() + if token is None: + token = self.request.session.new_csrf_token() + + return tags.hidden(name, value=token) + + def csrf_token(self, name=None): + """ + NOTE: this method was copied from `pyramid_simpleform.FormRenderer` + + Convenience function. Returns CSRF hidden tag inside hidden DIV. + """ + return HTML.tag("div", self.csrf(name), style="display:none;") + def render(self, **kwargs): kwargs['form'] = self if self.readonly: @@ -86,5 +114,8 @@ class AlchemyForm(Object): self.session.flush() def validate(self): + if invalid_csrf_token(self.request): + self.request.session.flash("Invalid CSRF token", 'error') + return False self.fieldset.rebind(data=self.request.params) return self.fieldset.validate() diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 0d2728cd..19187bbb 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -33,6 +33,17 @@ from formalchemy.helpers import content_tag from pyramid.renderers import render +def invalid_csrf_token(request): + """ + Returns boolean indicating whether the given request has an *invalid* CSRF token. + """ + if request.method == 'POST': + csrf_token = request.session.get_csrf_token() + if request.POST.get('_csrf') != csrf_token: + return True + return False + + class Form(object): """ Base class for all forms. diff --git a/tailbone/forms/simpleform.py b/tailbone/forms/simpleform.py index fa82b0b0..2fd481c3 100644 --- a/tailbone/forms/simpleform.py +++ b/tailbone/forms/simpleform.py @@ -33,7 +33,7 @@ from pyramid_simpleform import renderers from webhelpers.html import tags from webhelpers.html import HTML -from tailbone.forms import Form +from tailbone.forms import Form, invalid_csrf_token class SimpleForm(Form): @@ -52,6 +52,12 @@ class SimpleForm(Form): kwargs['form'] = FormRenderer(self) return super(SimpleForm, self).render(**kwargs) + def validate(self): + if invalid_csrf_token(self.request): + self.request.session.flash("Invalid CSRF token", 'error') + return False + return self._form.validate() + class FormRenderer(renderers.FormRenderer): """ diff --git a/tailbone/templates/forms/form.mako b/tailbone/templates/forms/form.mako index bf68a515..3baf812a 100644 --- a/tailbone/templates/forms/form.mako +++ b/tailbone/templates/forms/form.mako @@ -1,6 +1,7 @@ ## -*- coding: utf-8 -*-
${h.form(form.action_url, id=form.id or None, method='post', enctype='multipart/form-data')} + ${form.csrf_token()} ${form.render_fields()|n} diff --git a/tailbone/templates/login.mako b/tailbone/templates/login.mako index d9191026..0c2392b2 100644 --- a/tailbone/templates/login.mako +++ b/tailbone/templates/login.mako @@ -17,6 +17,7 @@
${form.begin(**{'data-ajax': 'false'})} ${form.hidden('referrer', value=referrer)} + ${form.csrf_token()} ${form.field_div('username', form.text('username'))} ${form.field_div('password', form.password('password'))} diff --git a/tailbone/templates/purchases/batches/receive_form.mako b/tailbone/templates/purchases/batches/receive_form.mako index 63d953a2..13312490 100644 --- a/tailbone/templates/purchases/batches/receive_form.mako +++ b/tailbone/templates/purchases/batches/receive_form.mako @@ -326,6 +326,7 @@
${form.begin(id='receiving-form')} + ${form.csrf_token()} ${h.hidden('mode')} ${h.hidden('expiration_date')} ${h.hidden('ordered_product')} diff --git a/tailbone/views/auth.py b/tailbone/views/auth.py index ed5fbf47..2b4cca73 100644 --- a/tailbone/views/auth.py +++ b/tailbone/views/auth.py @@ -101,8 +101,7 @@ class AuthenticationView(View): self.request.session.flash("{} is already logged in".format(self.request.user), 'error') return self.redirect(referrer) - form = Form(self.request, schema=UserLogin) - context = {'form': forms.FormRenderer(form), 'referrer': referrer, 'dialog': mobile} + form = forms.SimpleForm(self.request, UserLogin) if form.validate(): user = authenticate_user(Session(), form.data['username'], @@ -115,7 +114,12 @@ class AuthenticationView(View): return self.redirect(referrer, headers=headers) else: self.request.session.flash("Invalid username or password", 'error') - return context + + return { + 'form': forms.FormRenderer(form), + 'referrer': referrer, + 'dialog': mobile, + } def mobile_login(self): return self.login(mobile=True)