Skip to content

Instantly share code, notes, and snippets.

@j4mie
Last active October 23, 2020 13:40
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save j4mie/9055969 to your computer and use it in GitHub Desktop.
Save j4mie/9055969 to your computer and use it in GitHub Desktop.
Django CSRF error

Since Django 1.5.5, CSRF tokens are rotated on login. That makes it trivial to trigger a CSRF error in the following way:

  1. Open your app's login page in two different browser tabs.
  2. Log in using tab 1
  3. Log in using tab 2

The CSRF token sent along with the second login attempt (in a cookie) won't match the token that was embedded in the form, and so a CSRF error will be displayed.

It could be argued that the above is an odd/contrived thing to do, and so displaying a CSRF error here isn't too bad. Fine. But there's another, more subtle, way to trigger the same thing:

  1. Open your app's login page
  2. Click the login button twice, fairly fast

This problem is most obvious when the view that is displayed after login is quite slow to load. What happens is as follows:

  1. POST /login/ logs the user in, and performs the CSRF token rotation.
  2. The response is a 302, including a set-cookie header containing a new CSRF token, as well as a location header containing (say) /dashboard/.
  3. The browser sends a GET request to /dashboard/, which may take some time to load. While this page is loading, the login form is still displayed to the user.
  4. The user may get impatient (or just confused) and assume that they didn't click the login button correctly, so they click it again. This cancels the pending GET request to /dashboard/ and makes another POST to /login/. But now, the CSRF token in the user's cookie doesn't match the one in the form. So they see the CSRF error page.

This is actually quite a common thing - lots of users (especially non-technical people) double-click everything, because they don't know any different.

The obvious solution is to disable the login button with JavaScript after the first form submission, but this feels like a bit of a hack.

@djm
Copy link

djm commented Feb 18, 2014

I see what you mean now, it's funny how in all the years of using CSRF I've never clicked a button twice and yet my Mum does that every single time I ask her to click anything. So is it a usability bug? I would say so.

However, I believe the security issue trumps the usability one; can you suggest a way around this without crippling the CSRF protection? Github itself shares the same problem; on submitted an old token you get this error page.

@j4mie
Copy link
Author

j4mie commented Feb 18, 2014

A possible solution was suggested by @tomchristie. Django could only rotate the token in django.contrib.auth.login if the user isn't already logged in.

@j4mie
Copy link
Author

j4mie commented Feb 18, 2014

Tom clarified: the login view would have to be CSRF exempt for this to work, and manually perform the CSRF check after it's confirmed that the user isn't already logged in.

@j4mie
Copy link
Author

j4mie commented Feb 18, 2014

@tomchristie
Copy link

@jonmjo
Copy link

jonmjo commented Dec 16, 2017

I'm guessing this is what my users are doing. Thanks for the post!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment