Add initial support for CSRF token protection
This commit is contained in:
parent
11e78adaab
commit
ab09314ed3
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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'))}
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue