Fix Missing CSRF Token Issues with Flask
Learn how to fix bad request / CSRF token missing errors with Flask that stem from bugs with webkit based browsers.
There may come a time in your life where you’re absolutely sure that you have Flask-WTF configured properly in your application.
# You Did Everything the Docs Stated
For starters, you’ve instantiated and exported CsrfProtect
like so:
# myapp/extensions.py
from flask_wtf import CsrfProtect
csrf = CsrfProtect()
You’ve also imported it into your app.py
file:
# myapp/app.py
from myapp.extensions import csrf
Then you’ve initialized it onto your Flask app:
# myapp/app.py
def create_app():
app = Flask(__name__)
app.config.from_object('config.settings')
csrf.init_app(app)
Finally, you’ve included the proper tag in your form template:
<form action="{{ url_for('myform') }}" method="post" role="form">
{{ form.hidden_tag() }}
<!-- The rest of your form goes here. -->
</form>
Yet It Still Doesn’t Work and Flask Throws a CSRF Related Error
You may have tried to debug the issue by dropping this into your form’s route:
print('------ {0}'.format(request.form))
…and to your surprise the csrf_token
value is empty. WTF?
You’ve likely also opened your dev tools in your browser and went to the resources tab to take a look at your cookies. Oddly enough, it’s empty.
# What Does Your Dev Environment Look Like?
Chances are 2 things are happening in your environment:
- You’re running Flask inside of a Docker Machine or are using Vagrant / etc.
- You are using Chrome, Opera, Edge, IE11 and perhaps others (but not Firefox or Safari)
It may also be due to using an AWS EC2 instance’s public DNS hostname, but more on that later.
If your Flask server is not running on localhost
then in order to get Flask
to resolve URLs properly, you’ve likely modified the SERVER_NAME
value
somewhere.
For example, you might have something like this in config/settings.py
:
SERVER_NAME = '192.168.99.100:8000'
This is what I recommend my students to do in the Build a SAAS App with Flask course if they happen to be using Docker Toolbox because we use Docker.
# What Causes Bad Request CSRF Token Missing?
This problem happens because of 2 things.
Firstly, there’s a bug in webkit based browsers.
The spec for rejecting cookies states that domain names must be a fully qualified domain name with a TLD (.com, etc.) or be an exact IP address.
Update in 2017: It looks like the spec has changed to explicitly state no exact IP addresses, so the bits about Chrome being buggy are no longer accurate.
Update in 2020: I recently discovered this also affects AWS EC2 public DNS hostnames. To be honest I’m not sure why they are treated as a direct IP address, but you will get a missing CSRF token error if you use your instance’s public DNS hostname.
Chrome is too cool to adhere to specifications, so they decided to be more
strict and deny exact IP addresses. That means cookies won’t be set if you have
an IP address based SERVER_NAME
.
Ok, well that’s pretty lame but astute readers might be thinking how does
localhost
work because that doesn’t include a TLD.
That brings us to the second thing, and we can blame Flask for that.
The Flask author is a very talented developer and 99.9% of the time his decisions are for your benefit but in my opinion he screwed up with this one.
If you look at the Flask source to return the cookie domain he makes assumptions about your development environment.
Take a look at this block of code:
# Google chrome does not like cookies set to .localhost, so
# we just go with no domain then.
if rv == '.localhost':
rv = None
The Flask author is definitely aware of the problem but he hard codes a fix.
I can’t blame him because a lot of developers will be using localhost
so it
fixes the problem for those developers without them having to think about it.
Nowadays Docker and virtualized development environments are much more common, so IMO I’d like to see this turned into a Flask config option so users can set which domain can get ignored.
# Easily Fix the Problem for the Time Being
The quickest way to fix this problem in development would be to modify your
/etc/hosts
file.
OSX and Linux users can find that in /etc/hosts
and Windows users can find it
in C:\Windows\System32\drivers\etc\hosts
.
You will need to open the file with elevated privileges, meaning you’ll need to open it with sudo or Administrator privileges.
Add this line to the bottom of the file: 192.168.99.100 local.docker
Keep in mind, if your development IP address is not what’s listed above then make the necessary adjustment to use yours instead.
Also feel free to change local.docker
to anything you want, as long as it
includes a period so that it’s a valid FQDN with a TLD.
For example local.dev
or local.host
would be valid but localfoo
is not.
In the EC2 instance case, you should make a proper DNS entry on a real domain name and access your site there.
Updating Your Flask Config
The last thing you’ll need to do is change your SERVER_NAME
to match what we
just created in the /etc/hosts
file (or whatever your domain name is).
You’ll want to set: SERVER_NAME = 'local.docker:8000'
or whatever you used.
At this point you’re good to go and everything should work great.
# An Exercise in Debugging
This was an interesting issue to debug because as you may know, I create video tutorials and courses.
I recently created a course on Flask and I personally run Docker on Linux natively. I also happen to use FireFox which does adhere to the spec correctly.
When I ran through the material, everything worked great but then issues started to pour in from OSX users. Some OSX users were using Docker Toolbox to set up their Docker environment, so they were using IP based server names.
However, not all OSX users were reporting this issue because not everyone was using Chrome and Docker Toolbox. Needless to say, it wasn’t an obvious solution, especially since I had students connect to my server through ngrok and successfully submit the form.
Where as, when I connected to their server it worked for me because I was using FireFox. Then when I tried Chrome on their server it failed, and that lead me to eventually tie in that the problem had something to do with Chrome.
Other students reported the same problem on Safari so it seems to affect webkit in general.
The lesson here is that you should take nothing for granted when it comes to debugging. Even the slightest change in environment can cause drastic differences in output.