Skip to content

Commit

Permalink
Merge pull request #4 from janwijbrand/csrfprotection
Browse files Browse the repository at this point in the history
CRSF Protection in zope.formlib
  • Loading branch information
janwijbrand committed Nov 20, 2013
2 parents b06abfa + c325af0 commit b09b774
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Changes
4.3.0a2 (unreleased)
====================

- Support for CSRF protection.

- Added support for restricting the acceptable request method for the
form submit.

Expand Down
25 changes: 16 additions & 9 deletions src/zope/formlib/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,23 @@
permission="zope.Public"
template="widget_macros.pt"
/>

<adapter factory=".form.default_page_template" name="default" />
<adapter factory=".form.default_subpage_template" name="default" />
<adapter factory=".form.render_submit_button" name="render" />

<!-- Error view for 'Invalid' -->
<adapter
factory=".errors.InvalidErrorView"
permission="zope.Public"
/>
factory=".errors.InvalidErrorView"
permission="zope.Public"
/>

<!-- Error view for 'CSRF protection errors' -->
<adapter
factory=".errors.InvalidCSRFTokenErrorView"
provides="zope.publisher.interfaces.browser.IBrowserPage"
permission="zope.Public"
/>

<!-- For security proxied views -->
<class class=".form.Widgets">
Expand Down Expand Up @@ -295,7 +302,7 @@
permission="zope.Public"
/>

<!--
<!--
Need to repeat the above for Sequence to avoid being overridden by a
DAV widget. This suggests that we should be getting something
Expand Down Expand Up @@ -412,7 +419,7 @@
factory=".widgets.ChoiceCollectionInputWidget"
permission="zope.Public"
/>

<!-- FrozenSet + Choice -->
<adapter
for="zope.schema.interfaces.IFrozenSet
Expand Down Expand Up @@ -462,7 +469,7 @@
factory=".source.SourceMultiSelectSetWidget"
permission="zope.Public"
/>

<adapter
for="zope.schema.interfaces.IFrozenSet
zope.schema.interfaces.IIterableSource
Expand Down Expand Up @@ -501,7 +508,7 @@
factory=".widgets.MultiSelectSetWidget"
permission="zope.Public"
/>

<adapter
for="zope.schema.interfaces.IFrozenSet
zope.schema.interfaces.IVocabularyTokenized
Expand Down Expand Up @@ -573,7 +580,7 @@
factory=".source.SourceSequenceDisplayWidget"
permission="zope.Public"
/>

<adapter
for="zope.schema.interfaces.IAbstractSet
zope.schema.interfaces.ISource
Expand Down
29 changes: 28 additions & 1 deletion src/zope/formlib/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
from zope.i18n import Message
from zope.i18n import translate

from zope.formlib.interfaces import IWidgetInputErrorView
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.publisher.interfaces.browser import IBrowserPage
from zope.publisher.browser import BrowserPage
from zope.formlib.interfaces import IWidgetInputErrorView
from zope.formlib.interfaces import IInvalidCSRFTokenError


@implementer(IWidgetInputErrorView)
Expand All @@ -45,3 +48,27 @@ def snippet(self):
if isinstance(msg, Message):
msg = translate(msg, context=self.request)
return u'<span class="error">%s</span>' % escape(msg)


@implementer(IBrowserPage)
@adapter(IInvalidCSRFTokenError, IBrowserRequest)
class InvalidCSRFTokenErrorView(BrowserPage):

def update(self):
self.request.response.setStatus(403)
self.request.response.setHeader(
'Expires', 'Jan, 1 Jan 1970 00:00:00 GMT')
self.request.response.setHeader(
'Cache-Control', 'no-store, no-cache, must-revalidate')
self.request.response.setHeader(
'Pragma', 'no-cache')

def render(self):
msg = self.context.args[0]
if isinstance(msg, Message):
msg = translate(msg, context=self.request)
return escape(msg)

def __call__(self):
self.update()
return self.render()
45 changes: 45 additions & 0 deletions src/zope/formlib/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""
import binascii
import datetime
import os
import re
import sys
import pytz
Expand Down Expand Up @@ -43,6 +44,7 @@
from zope.formlib.interfaces import IInputWidget, IDisplayWidget
from zope.formlib.interfaces import WidgetsError, MissingInputError
from zope.formlib.interfaces import InputErrors, WidgetInputError
from zope.formlib.interfaces import InvalidFormError, InvalidCSRFTokenError

from zope.formlib import interfaces
from zope.i18nmessageid import MessageFactory
Expand Down Expand Up @@ -746,9 +748,48 @@ class FormBase(zope.publisher.browser.BrowserPage):

method = None

protected = False

csrftoken = None

def setPrefix(self, prefix):
self.prefix = prefix

def setUpToken(self):
self.csrftoken = self.request.getCookies().get('__csrftoken__')
if self.csrftoken is None:
# It is possible another form, that is rendered as part of
# this request, already set a csrftoken. In that case we
# should find it in the response cookie and use that.
setcookie = self.request.response.getCookie('__csrftoken__')
if setcookie is not None:
self.csrftoken = setcookie['value']
else:
# Ok, nothing found, we should generate one and set
# it in the cookie ourselves. Note how we ``str()``
# the hex value of the ``os.urandom`` call here, as
# Python-3 will return bytes and the cookie roundtrip
# of a bytes values gets messed up.
self.csrftoken = str(binascii.hexlify(os.urandom(32)))
self.request.response.setCookie(
'__csrftoken__',
self.csrftoken,
path='/',
expires=None, # equivalent to "remove on browser quit"
httpOnly=True, # no javascript access please.
)

def checkToken(self):
cookietoken = self.request.getCookies().get('__csrftoken__')
if cookietoken is None:
# CSRF is enabled, so we really should get a token from the
# cookie. We didn't get it, so this submit is invalid!
raise InvalidCSRFTokenError(_('Invalid CSRF token'))
if cookietoken != self.request.form.get('__csrftoken__', None):
# The token in the cookie is different from the one in the
# form data. This submit is invalid!
raise InvalidCSRFTokenError(_('Invalid CSRF token'))

def setUpWidgets(self, ignore_request=False):
self.adapters = {}
self.widgets = setUpWidgets(
Expand All @@ -760,6 +801,8 @@ def validate(self, action, data):
# Verify the correct request method was used.
if self.method.upper() != self.request.method.upper():
raise MethodNotAllowed(self.context, self.request)
if self.protected:
self.checkToken() # This form has CSRF protection enabled.
if self.ignoreContext:
context = None
else:
Expand All @@ -780,6 +823,8 @@ def resetForm(self):
form_reset = True

def update(self):
if self.protected:
self.setUpToken() # This form has CSRF protection enabled.
self.setUpWidgets()
self.form_reset = False

Expand Down
Loading

0 comments on commit b09b774

Please sign in to comment.