Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Fix Missing CSRF Token Issues with Flask

blog/cards/fix-missing-csrf-token-issues-with-flask.jpg

Learn how to fix bad request / CSRF token missing errors with Flask that stem from bugs with webkit based browsers.

Quick Jump: You Did Everything the Docs Stated | What Does Your Dev Environment Look Like? | What Causes Bad Request CSRF Token Missing? | Easily Fix the Problem for the Time Being | An Exercise in Debugging

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>

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:

  1. You’re running Flask inside of a Docker Machine or are using Vagrant / etc.
  2. 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.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per month (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments