Django, and the story of a CRSF cookie
Django ships with crsf protection. The other day however, I was left scratching my head over why the crsf cookie wasn’t present in my browser. At that time, the prototype was deployed on a .heroku.com domain, for testing.
A couple of things I went through, thinking the problem could be related to:
- The Host header, even though I set ALLOWED_HOSTS to [*]?
- Heroku.com being part of the Public Suffix List?
The fact that CSRF_COOKIE_SECURE was set to True, even though I also set SECURE_PROXY_SSL_HEADER to(‘HTTP_X_FORWARDED_PROTO’, ‘https’), as Heroku suggests.
- Any of the following settings
CSRF_TRUSTED_ORIGINS, CSRF_COOKIE_DOMAIN, and other CRSF settings, which I frankly never messed with before.
Nothing seemed to make that crsf cookie appear in my browser…
At the time, I actually thought the problem related to setting cookies in general, including the ones used for login, so I just quickly implemented a authentication hack using a hardcoded token and disabled crsf protection on every view with the crsf_exempt decorator.
I assumed the cookie problem would go away once I’ve deployed the app on our own infrastructure.
Couple of weeks later, on our own infrastructure, no crsf cookie present…
That time I was a less in a rush then when I deployed the prototype on Heroku, and started investigating a bit further.
After a read through Django’s CsrfViewMiddleware, and with the help of a few good old fashioned print statements, I was able to ascertain that inside process_response, the condition that request.META.CSRF_COOKIE_USED was False was hit, and the response was returned without a crsf cookie added to it.
So I started thinking, when is this request.META.CSRF_COOKIE_USED set to True?
It turns out it is set to True by a “get_token” function, which is part of Django’s internals. When is that function called? For example, when you use the “csrf_token” in your template.
Now it turns out, the good folks contributing to Django are aware of this, they’ve documented it, and even made available a decorator, aptly named “ensure_crsf_cookie”, which I have now added to my ‘home’ view, and indeed sets the crsf cookie.
Unfortunately, the answer pointing out this decorator, in response to the problem “Django CSRF Cookie Not Set”(itself the top result to the google search “django not setting crsf cookies”), has only received 2 upvotes.
Anyone smelling a hint of our love of complexity, while ignoring the simple and most obvious solution that is staring right at us?
A previous version of this article referred to the “requires_csrf_token”. That’s because I made a mistake reading both the django docs and the stackoverflow answer(which refers to the correct decorator).
It took me one test cycle to figure it out. Actually I almost just deployed the app without testing because I was so overconfident it would work…
I must be having bad day, or the django community have invented a few decorators too many?
Since crsf cookies are routinely reset on every login, do we really need this complicated logic? Should the default not simply be “set a crsf cookie on every response that doesn’t already include one?”, especially in this day and age when most people probably don’t use django templates that much anymore, and even less people actually render a form with a “crsf_token”?
In general, I see this as a lesson: our understanding of a given problem is, at any time, very very limited. This was actually a very simple problem, and I got it very wrong for quite some time.
Tools that allow us to manage our limited understanding of reality, like automated testing, building with “conventions over configurations” in mind, and simply sticking to the principle of least surprise, are not some ‘nice to have’ optional, they’re the difference between failure and success of any (software) project.