[Django]-Why does Django/Django REST Framework not validate CSRF tokens in-depth, even with enforce-CSRF?

8đź‘Ť

âś…

The specific contents of CSRF tokens in Django never matter, actually.

This reply by a Django security team member to a question similar to yours says this:

The way our CSRF tokens work is pretty simple. Each form contains a CSRF token, which matches the CSRF cookie. Before we process the protected form, we make sure that the submitted token matches the cookie. This is a server-side check, but it’s not validating against a stored server-side value. Since a remote attacker should not be able to read or set arbitrary cookies on your domain, this protects you.

Since we’re just matching the cookie with the posted token, the data is not sensitive (in fact it’s completely arbitrary – a cookie of "zzzz" works just fine), and so the rotation/expiration recommendations don’t make any difference. If an attacker can read or set arbitrary cookies on your domain, all forms of cookie-based CSRF protection are broken, full stop.

(Actually "zzzz" won’t work because of length requirements, but more on that later.) I recommend reading the entire mailing list message for a fuller understanding. There are explanations there about how Django is peculiar among frameworks because CSRF protections are independent of sessions.

I found that mailing list message via this FAQ item on the Django docs:

Is posting an arbitrary CSRF token pair (cookie and POST data) a vulnerability?

No, this is by design. Without a man-in-the-middle attack, there is no way for an attacker to send a CSRF token cookie to a victim’s browser, so a successful attack would need to obtain the victim’s browser’s cookie via XSS or similar, in which case an attacker usually doesn’t need CSRF attacks.

Some security audit tools flag this as a problem but as mentioned before, an attacker cannot steal a user’s browser’s CSRF cookie. “Stealing” or modifying your own token using Firebug, Chrome dev tools, etc. isn’t a vulnerability.

(Emphasis mine.)

The message is from 2011, but it’s still valid, and to prove it let’s look at the code. Both Django REST Framework’s SessionAuthentication and the ensure_csrf_cookie decorator use core Django’s CsrfViewMiddleware (source). In that middleware class’s process_view() method, you’ll see that it fetches the CSRF cookie (a cookie named csrftoken by default), and then the posted CSRF token (part of the POSTed data, with a fallback to reading the X-CSRFToken header). After that, it runs _sanitize_token() on the POSTed/X-CSRFToken value. This sanitization step is where the check for the correct token length happens; this is why you’re getting 403s as expected when you provide shorter or longer tokens.

After that, the method proceeds to compare the two values using the function _compare_salted_tokens(). If you read that function, and all the further calls that it makes, you’ll see that it boils down to checking if the two strings match, basically without regard to the values of the strings.

This behaviour seems to deviate from the regular Django login view, in which the contents of the CSRF do matter.

No, it doesn’t matter even in the built-in login views. I ran this curl command (Windows cmd format) against a mostly default Django project:

curl -v
  -H "Cookie: csrftoken=abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
  -H "X-CSRFToken: abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"
  -F "username=admin" -F "password=1234" http://localhost:8000/admin/login/

and Django returned a session cookie (plus a CSRF cookie, of course).


Just a note on the way you’re overriding SessionAuthentication.authenticate(): you probably already know this, but according to the DRF docs that method should return a (User, auth) tuple instead of None if the request has session data, i.e. if the request is from a logged-in user. Also, I think rotate_token() is unnecessary, because this code only checks for authentication status, and is not concerned with actually authenticating users. (The Django source says rotate_token() “should be done on login”.)

👤DJ Ramones

Leave a comment