Django, CORS, and CSRF Validation
by Justin Michalicek on Aug. 16, 2014, 7:41 p.m. UTCThis week I had the need to build some functionality on a Django site where a small part of the site would live on a subdomain. So, for example, if the main site was on example.com, this new bit needed to be on abc.example.com. The two parts of the site share sessions, cookies, and so on and abc.example.com needs to be able to make POST requests to AJAX form handlers on example.com (with a bit of an exception I'll get to in a moment).
For the domain and subdomain with share sessions the solution was fairly straightforward. The first step is setting the SESSION_COOKIE_DOMAIN and CSRF_COOKIE_DOMAIN values to .example.com in your settings.py. This allows the same cookie to be used for example.com and any subdomain of it. To simplify things further I used django-hosts. This let me run both sites from the exact same Django instances rather than needing to start up separate server instances with duplicated settings and yet still handle specific URLs, such as the root / URL, differently. I also wanted abc.example.com to only have routes for the specific paths it needed in place rather than duplicating URL routes unnecessarily for things like AJAX POST requests.
Where things got troublesome was using those AJAX form handlers cross domain. A bit of quick research pointed me at django-cors-headers, which basically just worked. In development, that is. After pushing out to a staging system which uses HTTPS I started getting 403 errors due to CSRF validation checks failing. This was further complicated by initially testing on Firefox and having the staging system using HTTP Basic Auth, which causes CORS requests which require a preflight check to fail because no Authorization header is sent (You can read more about that here if you're interested). So, anyway, once switching over to using Chrome for testing the 401 response issue was resolved but I was getting that 403 due to CSRF failures. That left me completely confused because I had just seen it work several times locally using a subdomain to main domain POST and Django had no issue with it.
It turns out that Django's CsrfViewMiddleware special cases HTTPS requests. Part of the additional checking is that the referer match the host exactly. Obviously this causes a bit of a problem with completely valid CORS requests. I really think that difference in handling and its ramifications should be called out more obviously in the documentation, which mention it, but only if you read a bit into the section on what could cause CSRF validation errors. There's really no reason to expect your code to suddenly start failing on HTTPS when it worked on HTTP. It seems to me that in addition to making that documentation harder to miss there should be a some settings which further tweak this behavior. The referrer checking makes complete sense, which the original developer discusses in the code comments and further here.
Fortunately I was not trying to go HTTP to HTTPS as the google groups discussion above is about, just one HTTPS domain to another. What I ended up doing was subclassing CsrfViewMiddleware and overriding the process_view() method. I wish it was broken up a bit more because I had to copy and paste more code than I wanted to just to make what amounted to a change of a few characters. At the line which checks the referrer I just updated it to also compare the referrer to the django-cors-headers CORS_ORIGIN_WHITELIST as well, since I had obviously already determined hosts in that whitelist to be safe enough to POST data to me and, in this case at least, are hosts under my control. Now I have comparison of the referrer to a list of trust hosts, including checking that the request came from HTTPS, but I can make my CORS requests on HTTPS with no failures.