April 10th, 2010
Rails, Django, and Just-Barely-Enough CSRF Protection
This week I found what I thought was a bug in Rails 2.3: it does not check the anti-CSRF authenticity token for AJAX requests. Due to years of experience with Rails I knew that this was not the previous behavior I have come to expect, so I dug around and learned that this behavior was intentionally changed some months back. Due to same-origin policy for XMLHttpRequests, the existence of the X-Requested-With header is considered sufficient validation of the request. Some details of the commit and comments on it can be found following this Github link. Much thanks to my friend Cory Scott of Matasano for bringing this to my attention.
Okay, fine—sort of. I have a beef with this.
Good security practice is about implementing layers of security, and this flies in the face of that. If certain other vulnerabilities are present, the most obvious one being HTTP response splitting, then CSRF is once again possible. Any vulnerability that could allow custom headers to be inserted in a CSRF request now makes them possible because the authenticity token is bypassed by a custom header.
In my opinion, if a site has data worth protecting with an anti-CSRF authenticity token, there’s no reason not to use it for all requests that can alter that data. Doing the bare minimum is what I call Just-Barely-Enough Security.
Note that Rails 3.0 handles CSRF protection differently (within Rack), and I have not examined it; the above comments apply only to Rails 2.x.
So it turns out that Rails is not the only web framework out there where the CSRF protection was in the just-barely-enough category. I have also been working on a Django 1.1 project recently, and in doing so I have found another category of just-barely-enough security (fixed in Django 1.2). Django 1.1 generates the authenticity token in a somewhat weak manner, but it is not weak enough that it is exploitable by itself. The weakness is that the authenticity token is the MD5 hash of a site-wide secret concatenated by the session ID (in contrast, Rails 2.3 and Django 1.2 generate a completely random token for each session). Because of how MD5 hashes are computed, an attacker could potentially learn the state of the MD5 computation up to the point of hashing the site-wide secret and himself compute any authenticity token given a session ID without further consulting the Django server. However, this is not an issue by default because Django’s session fixation protection prevents it from responding with session IDs that it did not generate, so the attacker cannot build up the necessary information to perform the attack. But if something were to be configured wrong on the Django server and a session fixation attack were to be possible, then the attacker would have more avenues for exploitation than just using session fixation on the victim directly.
Even worse would be a situation where the session fixation issue was found and repaired, but not before an attacker gleaned the necessary information to generate authenticity tokens for the site. The site administrator would think he closed the security hole, but his site may still be exploitable based on previously leaked information. That would be rather unfortunate.