On the recent Rails CVEs

Following up on my earlier tweet about the just-announced Rails vulnerabilities.

The case-insensitive filesystem hole is pretty much a non-issue for me (and almost everybody else who deploys on Linux) because all of the mainstream filesystems actually used on servers are case-sensitive.

The SQL-injection via "limit" clauses is not exploitable in my app seeing as there are no places where a user-supplied limit clause is provided.

The :encode => :javascript hole is also not an issue in my app as I don't use that functionality.

This leaves only the CSRF hole, and it's an issue that makes me rather sad.

CSRF background

If you're not familiar with CSRF, the Wikipedia article on the subject is a good starting point.

Attack vectors

In short, the attacker tries to trick the victim into submitting a request to a third-party site where the victim is currently logged in. For example, let's imagine that our attacker, Mallory, somehow makes the victim visit a page with a bogus <img> tag like this:

<img src="http://victim-bank.example.com/transfer?amount=500&to=mallory@example.com" />

If the user is logged in at "victim-bank.example.com" (for example, via a cookie), then the request will cause $500 to be transferred into Mallory's account, even though the victim did not intentionally visit his or her bank's website.

Note how the user doesn't need to actively click on the link in order to trigger the request; the mere loading of the <img> tag will be enough.

Note also how this is a "blind" attack. The attacker never sees the result of the request, but he nevertheless seeks to trigger some outcome that is desirable to him. In this case that outcome is transferring money to his account, but it could just as well be destroying data, or resetting a password, or any number of other things.

Mitigation

Rails provides built-in CSRF forgery for all POST requests made via forms. A one-time token is embedded into all forms using a hidden input, and any POST requests made without that token are rejected. The attacker can try to submit such a request, but it will fail, because the token will not be included.

Likewise, AJAX requests have historically not been vulnerable to CSRF attacks because the "same origin" policy enforced by the browser prevents any AJAX requests from being issued from other sites. That is, Mallory can craft JavaScript that will make an AJAX request to the victim's bank, but the browser won't actually let it submit. (If you allow Mallory to inject JavaScript onto the same site, then CSRF is the least of your worries, as you've already got a security hole large enough to drive a small barge through.)

This leaves GET requests like the one in the example above. The fix here is to follow the HTTP spec and ensure that all GET requests are side-effect free. Any action which has side-effects (like transferring money, resetting passwords, or deleting/mutating data) should instead be performable via POST only. The attacker can submit any GET requests he likes, but without any consequences.

Finally, we can limit the window of opportunity for such attacks by reducing the amount of time users stay logged in via cookies, or requiring them to re-authenticate before destructive or dangererous operations.

Flash, again

Unfortunately the official advisory doesn't go into detail about exactly how an attack would work but it does say this:

Certain combinations of browser plugins and HTTP redirects can be used to trick the user's browser into making cross-domain requests which include arbitrary HTTP headers specified by the attacker. An attacker can utilise this to spoof ajax and API requests and bypass the built in CSRF protection and successfully attack an application.

Looking at the actual commit which purports to close this attack vector (66ce3843), we see that the "certain combinations of browser plugins" probably refer to our good friend, Flash:

Unfortunately the previous method of browser detection and XHR whitelisting is unable to prevent requests issued from some Flash animations and Java applets. To ease the work required to include the CSRF token in ajax requests rails now supports providing the token in a custom http header:

X-CSRF-Token: ...

So this looks to be yet another example of Flash providing the means of executing an attack. Seriously, Flash has to be the suckiest piece of technology in the history of the web.

Update 1: More comments from Michael Koziarski (author of the advisory):

We've been deliberately vague in the description of the exploit so as not to provide ready-made exploit kits to attack other frameworks and rails applications which haven't been upgraded. We've been contacted since by a browser vendor and a few other frameworks requesting details. I'm sure the full details of how this can be exploited are already available in some circles but for now we're not going to make that job any easier.

I don't think providing any more details here will help anyone, it affects your users and is easily exploitable.

Update 2: Full disclosure here (mirrored here).

Why this makes me sad

This change means that we can no longer rely on the browser's "same origin" policy to keep AJAX requests safe. In turn, all pages with AJAX POST requests on them now require a unique token to be embedded within them, much like the existing CSRF protection in Rails provided for forms.

This is sad because it means it is no longer trivial to page-cache pages with AJAX POST requests on them, because if you do so you will end up leaking stale CSRF protection tokens into your cached pages, not just leaking information but also preventing the requests from working.

Page-caching is my favorite kind of caching in Rails, because it allows you to bypass the entire Rails stack and serve directly from your front-end of choice (eg. nginx). This is insanely fast (or rather was insanely fast); if your application server can do 100 requests/second when hitting the full Rails stack, it can probably easily handle 1,000 requests/second when serving up static files directly via nginx.

It's true that page-caching has its own problems, but they are not insurmountable. For example, if you want to display relative dates on your page-cached pages (eg. "2 days ago") then you either have to accept that such dates will become "stale" when cached, or instead display non-relative dates and use JavaScript to convert them, "on the fly", to relative dates.

As an another example, if you have a link at the top of each page that displays either "log in" or "log out" depending on whether the user has authenticated or not, you'll either need to go for a more generic "account" link (that takes you either the "log in" action, or the account overview) or use JavaScript to dynamically set up the appropriate link.

More generally, any page which is displayed differently to different users (for example, admin controls appearing on a page only for admin users) will require some special gymnastics, usually involving JavaScript, in order to be page-cacheable. Either that, or you just accept that some types of page just can't be page-cached.

You can sometimes use AJAX to side-step this issue, depending on the content you need to display. For example, just say you need to show one kind of comment form to logged-in users and another to anonymous visitors; you can pull down such a form via AJAX on demand, but only when the user actually clicks on the "add a comment" link. In this way most of your requests will be lightning fast, and the minority of users which actually want to comment will only trigger another, slower request once they actually hit the link.

But with this change we now have a whole new class of pages that can't be page-cached (pages with AJAX POST requests on them). It sucks that one of the most scalable caching methods is becoming less and less applicable.

And sadly, if this is a security bug in Flash and/or Java, it's sad that we framework users are now saddled with this uncomfortable, functionality-impairing workaround, rather than the hole being fixed upstream where it really lies (in Flash/Java).