Form validation fails due missing CSRF
Asked Answered
S

7

29

A few days ago I have reset my local flask environment without having captured the dependencies via a pip freeze before I deleted it. Hence I had to re-install the latest version of the entire stack.

Now out of the blue I am no longer able to validate with forms. Flask claims CSRF would be missing.

def register():
    form = RegisterForm()
    if form.validate_on_submit():
       ...
    return make_response("register.html", form=form, error=form.errors)

The first time I send a Get I retrieve an empty form.errors as expected. Now I fill out the form and submit it and form.errors is showing: {'csrf_token': [u'CSRF token missing']}

This is so strange. I wonder if Flask-WTF has changed and I am using it wrongly.

I can clearly see the form.CSRF_token exists, so why is it claiming it was missing?

CSRFTokenField: <input id="csrf_token" name="csrf_token" type="hidden" value="1391278044.35##3f90ec8062a9e91707e70c2edb919f7e8236ddb5">

I never touched the working template, but I post it here nonetheless:

{% from "_formhelpers.html" import render_field %}
{% extends "base.html" %}
{% block body %}
<div class="center simpleform">
    <h2>Register</h2>
    {% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
    <form class="form-signin" action="{{ url_for('register') }}" method=post>
        {{form.hidden_tag()}}
        <dl>
            {{ render_field(form.name) }}
            {{ render_field(form.email) }}
            {{ render_field(form.password) }}
            {{ render_field(form.confirm) }}
            <dd><input type=submit value=Register class='btn btn-primary'>
        </dl>
    </form>
</div>
{% endblock %}

Is this a new bug?

UPDATE:

I have reinstalled everything and the problem persists.

As Martijn suggested, I am debugging into the the following method in flask_wtf :

def validate_csrf_token(self, field):
        if not self.csrf_enabled:
            return True
        if hasattr(request, 'csrf_valid') and request.csrf_valid:
            # this is validated by CsrfProtect
            return True
        if not validate_csrf(field.data, self.SECRET_KEY, self.TIME_LIMIT):
            raise ValidationError(field.gettext('CSRF token missing'))

The last condition is raising the validation error.

field.data = "1391296243.8##1b02e325eb0cd0c15436d0384f981f06c06147ec"
self.SECRET_KEY = None (? Is this the problem)
self.TIME_LIMIT = 3600

And you were right the HMAC comparison fails....both values are in every time different.

return hmac_compare == hmac_csrf

I have both SECRET_KEY and CSRF_SESSION_KEY in my config defined.

Salient answered 1/2, 2014 at 17:17 Comment(10)
Are you accepting cookies? The CSRF architecture requires that the csrf_token value is present in the session and valid; it is a random value used to sign the token and on posting it is used to verify the CSRF token with the form (together with the server-side secret).Sianna
Yes, Neither Firefox nor Chrome are blocking cookies. I don't understand.Salient
So, to verify, you do see the a cookie named session set (provided you didn't set SESSION_COOKIE_NAME to something else)?Sianna
Yes. In Eclipse under Debug Watchlist, when I enter session, I get this: LocalProxy: <SecureCookieSession {'csrf_token': '2182effc89ce180a53622272d88d4466679920cd'}>Salient
The other failure mode is that the cookie is too old, but I suspect you didn't leave it as long as the default 1 hour expiration.Sianna
If you know how to debug (with PDB or a remote debugger), open up flask_wtf.csrf and put a breakpoint in validate_csrf to see why the token is being rejected.Sianna
Right, I posted your debugging options in my answer below.Sianna
self.SECRET_KEY is a per form secret key, it can safely be left at None at which point the app.config['WTF_CSRF_SECRET_KEY'] value is used (which defaults to app.secret_key).Sianna
I was having this issue for a long time, realized it was my SESSION_COOKIE_SECURE = True application setting. Hope this helps someone else.Evolutionist
If you happen to use FieldSets, you should put hidden_tag into each, and also into the main form. That's probably because each FieldSet is evaluated separately. Also keep in mind the Troubleshooting notice below Flask-WTF CSRF description, and import Form class from flask.ext.wtf insted of wtforms!Jughead
S
28

The Flask-WTF CSRF infrastructure rejects a token if:

  • the token is missing. Not the case here, you can see the token in the form.

  • it is too old (default expiration is set to 3600 seconds, or an hour). Set the TIME_LIMIT attribute on forms to override this. Probably not the case here.

  • if no 'csrf_token' key is found in the current session. You can apparently see the session token, so that's out too.

  • If the HMAC signature doesn't match; the signature is based on the random value set in the session under the 'csrf_token' key, the server-side secret, and the expiry timestamp in the token.

Having eliminated the first three possibilities, you need to verify why the 4th step fails. You can debug the validation in flask_wtf/csrf.py file, in the validate_csrf() function.

For your setup, you need to verify that the session setup is correct (especially if you don't use the default session configuration), and that you are using the correct server-side secret. The form itself could have a SECRET_KEY attribute set but is not stable across requests, or the app WTF_CSRF_SECRET_KEY key has changed (the latter defaults to the app.secret_key value).

The CSRF support was added in version 0.9.0, do check out the specific CSRF protection documentation if you upgraded. The standard Flask-WTF Form class includes the CSRF token as a hidden field, rendering the hidden fields is enough to include it:

{{ form.hidden_tag() }}
Sianna answered 1/2, 2014 at 18:4 Comment(2)
Thanks Martijn, your help is really appreciated. May you please have a look at my updated question?Salient
In my case, the app was working fine on local machine but it crashed while hosting. This was due to incorrect session setting as described by the author. Please set a constant secret key in flask app if you're running into same problem.Fluctuate
S
18

I finally found the problem after nearly a day working on it. :( Big thanks to Martijn though for his help.

The actual problem lies in the way the latest flask_wtf.csrf is working. The makers have overhauled it completely.

You have to replace all {{form.hidden_tag()}} in your templates with <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>.

And you have now to enable CSRF protection explicitly by adding CsrfProtect(app).

The documentation is now obviously reflecting that, but I didn't know this has changed and was chasing ghosts.

Its a big problem with deprecated functionality without notifying the developer somehow. Anyone that upgrades now to the latest version, will be chasing ghosts like I did. But its also my fault not having taken a snapshot of my dependencies. Lesson learned the hard way.

Salient answered 1/2, 2014 at 22:58 Comment(9)
You don't have to replace the hidden_tag(), but the CsrfProtect(app) is certainly required.Sianna
Also, self.SECRET_KEY is the per-form secret, when it is set to None, the app secret is used.Sianna
Thanks Martijn, I found another issue. I am deploying it on GAE. It seems flask_wtf is colliding with Flask-DebugToolbar. When I disable toolbar = DebugToolbarExtension(app), CSRF is working on developer environment. But only those developers are affected that are using flask-appengine-template. :)Salient
Interesting. My current project is using Flask on GAE, but I didn't use any templates, nor use Flask-DebugToolbar. If I do, I'll watch out for any issues.Sianna
I was having the same issue. adding WTF_CSRF_SECRET_KEY solved the problem.Slapdash
os.urandom() is bad idea for SECRET_KEY generation when using gunicorn with workers greater than 1Entity
I now have several projects with both the debug toolbar and CSRF protected forms. If you are only using FlaskForm forms, CSRFProtect is not needed at all and all you need in a template is the form.hidden_tag() output. Put differently; you only need to use "{{ csrf_token() }} and CSRFProtect for views that do not use a FlaskForm form object!Sianna
Instead of CsrfProtect(app) you can use lazy loading with csrf = CsrfProtect() csrf.init_app(app).Didier
doesn't work for me :/Jettiejettison
G
6

At the time of creating the app:

from flask_wtf.csrf import CsrfProtect

csrf = CsrfProtect()

app = Flask(__name__)   

...

csrf.init_app(app)

...
Gaylord answered 31/8, 2018 at 13:46 Comment(0)
O
2

For me, the problem was not coming from Flask-WTF being badly configured, or a missing token. It was coming from the environment variables.

If your Flask server is not running on localhost then in order to get Flask to work properly, you need to set a SERVER_NAME environment variable. You’ve likely forgotten to modify the SERVER_NAME value somewhere.

For example, you could have something like this in config/settings.py:

SERVER_NAME = 'my-domain.com'

For more information, check out this great resource

Opossum answered 7/8, 2018 at 21:9 Comment(0)
H
2

Using FieldLists?

When using FieldList, there is another source of the error that unfortunately, I found after hours of debugging:

class Subform(FlaskForm):
    """Parent form."""
    text = StringField("Text")


class Maniform(FlaskForm):
    """Parent form."""
    laps = FieldList(
        FormField(Subform),
        min_entries=1,
        max_entries=30
    )

This will not handle the CSRF correctly because Subform should inherit from wtforms.Form

class Subform(Form):
    """Parent form."""
    text = StringField("Text")

Fixing this bug solved my problems.

Hundredweight answered 17/11, 2021 at 14:42 Comment(2)
I was looking just for this. Every other forms were working fine, except the one implemented using FieldList. Thanks a lot!Bolivia
As you said hours of debugging and searching, now I know and wrote a comment in my app!Attune
S
2

It's a little different than you. In my case, it was simply because I had {{ form.csrf_token }} before the form tag.

{{ form.csrf_token }}
<form class="container" method="POST" id="form"> ... </form>

So I put it in the form tag

<form class="container" method="POST" id="form"> {{ form.csrf_token }} </form>

and that's it !

Sonata answered 8/4, 2022 at 22:7 Comment(0)
P
0

I experienced this issue because I used a hyphen instead of an underscore for the element name.

<!-- Wrong -->
<input type="hidden" name="csrf-token" value="{{csrf_token()}}">

<!-- Correct -->
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">

For non-WTForms, I guess the CSRF extension looks through the DOM for an element with name: 'csrf_token' not 'csrf-token'.

This is an error that anyone could easily make. In one of my other templates, I changed the input name to include a hyphen because I was sending the formData with Fetch API. I unknowingly copied the same input tag and that was what led to the error.

This means that the name attribute value could be anything as long as one is responsible for sending the token value to the backend themselves. But if you want the flask_wtf.CSRF extension to implicitly protect your non-WTForm, you have to do it the way it's expected:

<input name="csrf_token"/>

If you're still experiencing CSRF issues in Flask, I'll refer you to this great SO answer.

Puentes answered 10/10, 2023 at 20:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.