Add initial support for CSRF token protection

This commit is contained in:
Lance Edgar 2016-12-14 15:41:15 -06:00
parent 11e78adaab
commit ab09314ed3
8 changed files with 61 additions and 6 deletions

View file

@ -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

View file

@ -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()

View file

@ -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.

View file

@ -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):
"""

View file

@ -1,6 +1,7 @@
## -*- coding: utf-8 -*-
<div class="form">
${h.form(form.action_url, id=form.id or None, method='post', enctype='multipart/form-data')}
${form.csrf_token()}
${form.render_fields()|n}

View file

@ -17,6 +17,7 @@
<div class="form">
${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'))}

View file

@ -326,6 +326,7 @@
<div class="form-wrapper">
${form.begin(id='receiving-form')}
${form.csrf_token()}
${h.hidden('mode')}
${h.hidden('expiration_date')}
${h.hidden('ordered_product')}

View file

@ -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)