Skip to content

Commit

Permalink
csrf-error view
Browse files Browse the repository at this point in the history
  • Loading branch information
janwijbrand committed Oct 8, 2013
1 parent fa7e79d commit c440a42
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 18 deletions.
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
25 changes: 24 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,23 @@ 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.doc
if isinstance(msg, Message):
msg = translate(msg, context=self.request)
return escape(msg)
6 changes: 3 additions & 3 deletions src/zope/formlib/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +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
from zope.formlib.interfaces import InvalidFormError, InvalidCSRFTokenError

from zope.formlib import interfaces
from zope.i18nmessageid import MessageFactory
Expand Down Expand Up @@ -784,11 +784,11 @@ def checkToken(self):
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 InvalidFormError('CSRF token incorrect')
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 InvalidFormError('CSRF token incorrect')
raise InvalidCSRFTokenError(_('Invalid CSRF token'))

def setUpWidgets(self, ignore_request=False):
self.adapters = {}
Expand Down
6 changes: 3 additions & 3 deletions src/zope/formlib/form.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2058,7 +2058,7 @@ If for some reason the cookie is not set, the form will raise an error::
>>> _ = myform() #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
InvalidFormError: CSRF token incorrect
InvalidCSRFTokenError: Invalid CSRF token

As an attacker cannot read the cookie value, he can only guess the
corresponding form value, that is hard get right, so most proably wrong::
Expand All @@ -2072,7 +2072,7 @@ corresponding form value, that is hard get right, so most proably wrong::
>>> _ = myform() #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
InvalidFormError: CSRF token incorrect
InvalidCSRFTokenError: Invalid CSRF token

When the form value is missing altogether, the form obviously raises an error
too::
Expand All @@ -2085,7 +2085,7 @@ too::
>>> _ = myform() #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
InvalidFormError: CSRF token incorrect
InvalidCSRFTokenError: Invalid CSRF token

To repeat: this protection works as long as the cookie value is identical to
the submitted form value. No state is kept on the server. We can demonstrate
Expand Down
14 changes: 12 additions & 2 deletions src/zope/formlib/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,19 @@ def doc():
@implementer(IInvalidFormError)
class InvalidFormError(Exception):
"""The form submit could not be validated.
"""

class IInvalidCSRFTokenError(Interface):

def doc():
"""The form submit could not be handled as the CSRF token is missing
or incorrect.
"""

For example when the CSRF token is incorrect or does not existed
for "protected" form components.
@implementer(IInvalidCSRFTokenError)
class InvalidCSRFTokenError(InvalidFormError):
"""The form submit could not be handled as the CSRF token is missing
or incorrect.
"""

class IWidgetInputError(Interface):
Expand Down

0 comments on commit c440a42

Please sign in to comment.