Permalink
Browse files

Add "CSRF vulnerability in Ruby on Rails 2.3.10 & 3.0.3" post.

  • Loading branch information...
1 parent fa6aba5 commit 686b3ef9cd403db1104fe0234c772840a771adf1 @jasoncodes committed Feb 10, 2011
Showing with 86 additions and 0 deletions.
  1. +86 −0 _posts/rails-csrf-vulnerability.markdown
@@ -0,0 +1,86 @@
+---
+layout: post
+title: CSRF vulnerability in Ruby on Rails 2.3.10 & 3.0.3
+date: 2011-02-10
+---
+
+Yesterday Rails 3.0.4 and Rails 2.3.11 were released with patches for a few security issues. The two you're most likely to be affected by are [CSRF Protection Bypass in Ruby on Rails (CVE-2011-0447)](http://groups.google.com/group/rubyonrails-security/browse_thread/thread/2d95a3cc23e03665) and [Potential SQL Injection in Rails 3.0.x (CVE-2011-0448)](http://groups.google.com/group/rubyonrails-security/browse_thread/thread/b658902cf6bf4eed). I'll be looking at the CSRF issue here. The [Riding Rails blog](http://weblog.rubyonrails.org/) has a [post on the CSRF protection patch](http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails) which is worth reading.
+
+# The problem [problem]
+
+It has been discovered that browser plugins such as Flash and Java in some circumstances can bypass the same origin policy and send requests with a `X-Requested-With: XMLHttpRequest` header and POSTing with a different content type than normally used by forms (i.e. `application/javascript` instead of `application/x-www-form-urlencoded` or `multipart/form-data`). These methods are used by Rails to detect the difference between form POSTs which need the authenticity token field and AJAX requests (from jQuery or Prototype) which do not. This seemed like a good way to prevent cross-site request forgeries since these headers can't be set for a POST via a `<form/>` and the same origin policy prevents `XMLHttpRequest` requests from other sites.
+
+# The fix [fix]
+
+Rails 3.0.4 and Rails 2.3.11 have patches for this issue in commits [ae19e41](https://github.com/rails/rails/commit/ae19e4141f27f80013c11e8b1da68e5c52c779ea "ae19e4141f27f80013c11e8b1da68e5c52c779ea") and [7e86f9b](https://github.com/rails/rails/commit/7e86f9b4d2b7dfa974c10ae7e6d8ef90f3d77f06 "7e86f9b4d2b7dfa974c10ae7e6d8ef90f3d77f06") respectively. With this patch, Rails now marks all non-GET requests which don't contain a valid authenticity token as unverified. This means AJAX POST requests will now need to pass send the authenticity token in a HTTP header.
+
+The method for handling invalid requests (that is, POSTs without a valid authenticity token) has also changed with this patch. Instead of raising an `InvalidAuthenticityToken` exception, Rails now calls `handle_unverified_request` which by default clears your session data. The idea of this is that unverified requests cannot do any damage if they don't have access to any persistent state (like your user session).
+
+A nice thing about this new setup is if you use HTTP authentication or an `X-API-Token` type header for API requests, they'll continue to function fine as these authentication systems don't use session data.
+
+# Authlogic [authlogic]
+
+Authlogic however stores authentication data in a cookie which by default is called `user_credentials`. This is separate from session data so we have to override `handle_unverified_request` in the `ApplicationController` to clear this ourselves.
+
+{% highlight ruby %}
+def handle_unverified_request
+ super
+ cookies.delete 'user_credentials'
+end
+{% endhighlight %}
+
+Please do not blindly upgrade to 2.3.11/3.0.4 without carefully testing your authentication system with POST requests lacking an authentication token. Without adding the above code we would effectively lose CSRF protection as the app will still see the authentication and process the request where there's an invalid or missing authenticity token.
+
+# Devise [devise]
+
+If you use Devise's "remember me" functionally, you'll need to clear the persistent cookie. By default this is called `remember_user_token`. If you're making use of multiple Warden scopes, make sure you handle those as well.
+
+{% highlight ruby %}
+def handle_unverified_request
+ super
+ cookies.delete 'remember_user_token'
+end
+{% endhighlight %}
+
+# Ajax requests (jQuery) [ajax]
+
+Since `X-Requested-With: XMLHttpRequest` is no longer enough to verify authenticity to Rails, we'll need to pass the authenticity token with each non-GET Ajax request. New to 2.3.11 is the `csrf_meta_tag` form helper which has been backported from Rails 3. If you don't already have `csrf_meta_tag` in your layout, add the following to the `%head` section of your (hopefully [Haml](http://haml-lang.com/)) layouts:
+
+{% highlight haml %}
+%head
+ = csrf_meta_tag
+{% endhighlight %}
+
+This adds a couple of meta attributes to every page which jQuery can look up to get the authenticity token. The second part of making Ajax requests work again is to set the `X-CSRF-Token` header on all Ajax requests with the authenticity token. Add the following to your main `application.js` file:
+
+{% highlight javascript %}
+$.ajaxSetup({
+ 'beforeSend': function(xhr) { xhr.setRequestHeader('X-CSRF-Token', $('meta[name=csrf-token]').attr('content')); }
+});
+{% endhighlight %}
+
+# Avoiding authentication denial of service (DoS) [dos]
+
+One big thing I don't like about this fix is that it opens up an opportunity for a remote host to kill your login session. This can happen because any POST request with an invalid or missing authenticity token will kill your session data. Any other site with a `<script/>` block could POST a form to your site to log you out. It's almost as bad as making your logout link a GET instead of a POST.
+
+Because of this I decided I'd rather have the previous behaviour of raising an `InvalidAuthenticityToken` exception to reject unverified requests completely:
+
+{% highlight ruby %}
+def handle_unverified_request
+ raise ActionController::InvalidAuthenticityToken
+end
+{% endhighlight %}
+
+A side affect of this is that any POST requests outside of your web UI (such as to an API) will now fail as they aren't passing in the `X-CSRF-Token` header. We can get the best of both worlds by raising `InvalidAuthenticityToken` for web requests and clearing session for all other requests.
+
+{% highlight ruby %}
+def handle_unverified_request
+ content_mime_type = request.respond_to?(:content_mime_type) ? request.content_mime_type : request.content_type
+ if content_mime_type && content_mime_type.verify_request?
+ raise ActionController::InvalidAuthenticityToken
+ else
+ super
+ cookies.delete 'user_credentials'
+ end
+end
+{% endhighlight %}

0 comments on commit 686b3ef

Please sign in to comment.