Hi everybody,
TL;DR: A simple change can make Django's CSRF protection a little better; an
additional, slightly less simple one, can also make it look better.
Django's CSRF protection scheme is a bit unusual; unlike most such schemes, it
does not rely on a value stored in the server that needs to be matched by a
submitted token and is replaced with every submission, but rather on a
constant value stored in a cookie. This generally works (for details of how
and under what conditions exactly, see [1]), but has two minor problems:
1) It is unusual, and in particular diverges from what OWASP[2] recommends[3];
as a result, security analysts often think it is not secure. They have been
proven wrong in all cases members of core are aware of, but proving it again
and again is a nuisance, and there may be bad PR related to this.
2) It carries a "second-order" vulnerability: If your site has been
compromised (XSS, Man-in-the-middle, or server compromise) then you become
persistently vulnerable to CSRF. All of these vulnerabilities are way worse
than CSRF and render all CSRF protection schemes worthless while they last;
the point is *not* that they allow CSRF, but rather that they allow CSRF to be
performed after the main hole has been plugged. This is because the attacker
can use the main vulnerability to "steal", or even set, csrftoken cookie
values, which they can then use later. After a successful attack of this
magnitude, you need to reset the csrftoken cookies of all users, and this is
neither obvious nor straightforward to do.
Django's unique scheme does have two advantages over the more common
solutions, which we would like to keep:
1) It is not tied to sessions, users, or site-stored per-user data, allowing
CSRF protection to a wider range of users
2) It avoids the problem of having only one "current" token, which causes the
submission of one form to invalidate forms open in other browser tabs.
To improve on both problem issues, while keeping the advantages, I suggest the
following modifications:
a) Use a signed cookie for csrftoken -- using Django's existing signing
facility[4], this means signing the cookie with the SECRET_KEY from the
settings; so that an attacker cannot set arbitrary cookies, and changing the
SECRET_KEY after a compromise immeiately invalidates csrftoken cookies.
b) Optionally allowing time-limited CSRF tokens. Such tokens will be generated
by adding a parameter of maximum age to the csrftoken tag, and by marking view
methods (specifically with a decorator, or globally with a setting) as
requiring timed tokens. When this is used, the posted token value will need to
be different from the cookie value -- to keep advantage 2, the cookie will
still be constant, and expiry time will only be present in the submitted
token[5]. This method breaks the current way we do CSRF-protected AJAX, so it
will likely stay optional (and opt-in).
As you may guess, signing the cookie adds an actual iota of security. Adding
expiry adds very little -- if an attacker has access to the cookie, they can
usually just ask the site to generate valid tokens for them, so getting any
real protection will require annoyingly short expiry times. But the fact that
an attacker needs this extra step makes it a tiny bit harder for them and
makes their actions a tiny bit more detectable; and having a constantly-
changing CSRF token may make the whole thing look a little better to naive
analysts.
I had some help and guidance in drafting this proposal -- you can credit
Donald Stufft, mostly, for any egregious blunder I didn't make. I am still
responsible for the ones I did make.
Your comments are welcome,
Shai.
[1] https://docs.djangoproject.com/en/dev/ref/contrib/csrf/ -- in particular,
"how it works" and "limitations"
[2] https://www.owasp.org
[3]
https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet
[4] https://docs.djangoproject.com/en/dev/topics/signing/
[5] django.core.signing.TimestampSigner signs content with the time of the
signature, and then takes a max_age in its unsign() method; the suggested
method would go the other way around, timestamping the token with the time of
expiry, to allow checking without using data stored on the server (and to
allow different forms to use different max-age values).
--
You received this message because you are subscribed to the Google Groups
"Django developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/django-developers.
For more options, visit https://groups.google.com/groups/opt_out.