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 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 .simpleform import SimpleForm, FormRenderer
from .alchemy import AlchemyForm from .alchemy import AlchemyForm
from .fields import AssociationProxyField from .fields import AssociationProxyField

View file

@ -30,8 +30,10 @@ from rattail.core import Object
import formalchemy as fa import formalchemy as fa
from pyramid.renderers import render from pyramid.renderers import render
from webhelpers.html import HTML, tags
from tailbone.db import Session from tailbone.db import Session
from tailbone.forms import invalid_csrf_token
class TemplateEngine(fa.templates.TemplateEngine): class TemplateEngine(fa.templates.TemplateEngine):
@ -54,11 +56,12 @@ class AlchemyForm(Object):
allow_successive_creates = False 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) super(AlchemyForm, self).__init__(**kwargs)
self.request = request self.request = request
self.fieldset = fieldset self.fieldset = fieldset
self.session = session self.session = session
self.csrf_field = csrf_field
def _get_readonly(self): def _get_readonly(self):
return self.fieldset.readonly return self.fieldset.readonly
@ -70,6 +73,31 @@ class AlchemyForm(Object):
def successive_create_label(self): def successive_create_label(self):
return "%s and continue" % self.create_label 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): def render(self, **kwargs):
kwargs['form'] = self kwargs['form'] = self
if self.readonly: if self.readonly:
@ -86,5 +114,8 @@ class AlchemyForm(Object):
self.session.flush() self.session.flush()
def validate(self): 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) self.fieldset.rebind(data=self.request.params)
return self.fieldset.validate() return self.fieldset.validate()

View file

@ -33,6 +33,17 @@ from formalchemy.helpers import content_tag
from pyramid.renderers import render 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): class Form(object):
""" """
Base class for all forms. 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 tags
from webhelpers.html import HTML from webhelpers.html import HTML
from tailbone.forms import Form from tailbone.forms import Form, invalid_csrf_token
class SimpleForm(Form): class SimpleForm(Form):
@ -52,6 +52,12 @@ class SimpleForm(Form):
kwargs['form'] = FormRenderer(self) kwargs['form'] = FormRenderer(self)
return super(SimpleForm, self).render(**kwargs) 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): class FormRenderer(renderers.FormRenderer):
""" """

View file

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

View file

@ -17,6 +17,7 @@
<div class="form"> <div class="form">
${form.begin(**{'data-ajax': 'false'})} ${form.begin(**{'data-ajax': 'false'})}
${form.hidden('referrer', value=referrer)} ${form.hidden('referrer', value=referrer)}
${form.csrf_token()}
${form.field_div('username', form.text('username'))} ${form.field_div('username', form.text('username'))}
${form.field_div('password', form.password('password'))} ${form.field_div('password', form.password('password'))}

View file

@ -326,6 +326,7 @@
<div class="form-wrapper"> <div class="form-wrapper">
${form.begin(id='receiving-form')} ${form.begin(id='receiving-form')}
${form.csrf_token()}
${h.hidden('mode')} ${h.hidden('mode')}
${h.hidden('expiration_date')} ${h.hidden('expiration_date')}
${h.hidden('ordered_product')} ${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') self.request.session.flash("{} is already logged in".format(self.request.user), 'error')
return self.redirect(referrer) return self.redirect(referrer)
form = Form(self.request, schema=UserLogin) form = forms.SimpleForm(self.request, UserLogin)
context = {'form': forms.FormRenderer(form), 'referrer': referrer, 'dialog': mobile}
if form.validate(): if form.validate():
user = authenticate_user(Session(), user = authenticate_user(Session(),
form.data['username'], form.data['username'],
@ -115,7 +114,12 @@ class AuthenticationView(View):
return self.redirect(referrer, headers=headers) return self.redirect(referrer, headers=headers)
else: else:
self.request.session.flash("Invalid username or password", 'error') self.request.session.flash("Invalid username or password", 'error')
return context
return {
'form': forms.FormRenderer(form),
'referrer': referrer,
'dialog': mobile,
}
def mobile_login(self): def mobile_login(self):
return self.login(mobile=True) return self.login(mobile=True)