Form validation fails due missing CSRF
Question:
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.
Answers:
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() }}
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.
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
At the time of creating the app:
from flask_wtf.csrf import CsrfProtect
csrf = CsrfProtect()
app = Flask(__name__)
...
csrf.init_app(app)
...
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.
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 !
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.
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() }}
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.
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
At the time of creating the app:
from flask_wtf.csrf import CsrfProtect
csrf = CsrfProtect()
app = Flask(__name__)
...
csrf.init_app(app)
...
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.
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 !