minou
minou

Reputation: 16563

CSRF with WTforms and webapp2

The WTForms documentation gives a great example for implementing CSRF with Flask:

class MyBaseForm(Form):
    class Meta:
        csrf = True
        csrf_class = SessionCSRF
        csrf_secret = app.config['CSRF_SECRET_KEY']

        @property
        def csrf_context(self):
            return session

I'd like to do the same thing but with a webapp2 session instead of a Flask session.

The csrf_context is a shortcut so that you don't have to pass the session every time that you create a form. Does anyone know how to create such a shortcut for a webapp2 session?

Without this shortcut, you need to do something like this every time you create a form:

form = MyForm(meta={'csrf_context': self.session})

which is some pretty awkward syntax that I'd prefer to avoid.

Upvotes: 3

Views: 386

Answers (1)

minou
minou

Reputation: 16563

I've come up with a solution that reduces the awkward syntax described in my question. I've modified __init__ of my subclass of wt.Form like this:

class MyBaseForm(wt.Form):
    class Meta:
        csrf = True
        csrf_class = SessionCSRF
        csrf_secret = settings.SESSION_KEY
        csrf_time_limit = timedelta(minutes=60)

    def __init__(self, *args, **kwargs):
        if "session" in kwargs:
            super(MyBaseForm, self).__init__(
                *args, meta={'csrf_context': kwargs["session"]}, **kwargs)
        else:
            super(MyBaseForm, self).__init__(*args, **kwargs)

Now, when I create a form, I can do this:

form = MyForm(session=self.session)

instead of the awkward syntax shown in the question.

For processing POSTed form data, I've figured out another technique to simplify processing.

First, I create a form with no fields other than the CSRF field:

class NoFieldForm(MyBaseForm):
    pass

Second, I create a decorator that is used to check CSRF:

def check_csrf(func):
    def wrapper(*args, **kwargs):
        handler = args[0]
        session = handler.session
        request = handler.request
        f = forms.NoFieldForm(request.POST, session=session)
        f.validate()
        if f.csrf_token.errors:
            msg = "The CSRF token expired. Please try again. "
            self.session["msg"] = msg
            self.redirect(self.request.path)
        else:
            func(*args, **kwargs)
    return wrapper

Third, I decorate all my POST handlers:

class SomeHandler(webapp2.RequestHandler):
    @check_csrf
    def post(self):
        pass

Let me give some explanation. The decorator (via the call to the post webhandler) is going to receive some form data, but for purposes of CSRF checking we are going to throw away all form data except for CSRF. We can make it generic by using the NoFieldForm which ignores any other form data that is present. I use wtforms to check to make sure the CSRF form field and session token match and that the session token isn't expired.

If the CSRF passes, then we call the handler for normal processing to handle the specific form. If the CSRF fails, then we don't call the handler at all, and in my example, we redirect back to where we came from with an error message.

Upvotes: 2

Related Questions