This post is the fifth in a series, 12 Days of HaXmas, where we take a look at some of more notable advancements in the Metasploit Framework over the course of 2013.

Several weeks ago, Egor Homakov wrote a blog post pointing out a common info leak vulnerability in many Rails apps that utilize Remote JavaScript. The attack vector and implications can be hard to wrap your head around, so in this post I'll explain how the vulnerability occurs and how to exploit it.

What is Remote Javascript?

Remote JavaScript (RJS) was a pattern prescribed by Rails < 2 to implement dynamic web sites. In RJS the user-facing parts of a website (HTML and JS) act as a "dumb client" for the server: when dynamic action is needed, the client calls a JavaScript helper that sends a request to the server. The server then performs the necessary logic and generates and responds with JavaScript code, which is sent back to the client and eval()'d.

The RJS approach has some advantages, as rails creator dhh points out in a recent blog post. However, suffice it to say that RJS breaks down as soon as you need complex client-side code, and a server API that responds with UI-dependent JavaScript is not very reusable. So Rails mostly has moved away from the RJS approach (JSON APIs and client-heavy stacks are the new direction), but still supports RJS out of the box.

So what's the problem?

Unfortunately, RJS is insecure by default. Imagine a developer on a Rails app that uses RJS is asked to make an Ajax-based login pop-up page. Following the RJS pattern, the developer would write some JavaScript that, when the "Login" link is clicked, asks the remote server what to do. The developer would add a controller action to the Rails app that responds with the JavaScript required to show the login form:

class Dashboard
  def login_form
    respond_to do |format|
      format.js do
        render :partial => 'show_login_form'
      end
    end
  end
end

Following the RJS pattern, the show_login_form.js.erb partial returns some JavaScript code to update the login form container:

$("#login").show().html("<%= escape_javascript(render :partial => 'login/form')")

Which, when rendered, produces code such as:

$("#login").show().html("
  <form action='/login' method='POST'>
  <input type='hidden' name='auth_token' value='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'>
  <table>
      <tr>
          <td>Name</td>
          <td><input type='text'></td>
      </tr>
      <tr>
          <td>Password</td>
          <td><input type='password'></td>
      </tr>
  </table>
</input>")

Now imagine user Tom is logged into the Rails app (which we'll say is served from railsapp.com). An unrelated website attacker.com might serve Tom the following code:

<html>
  <body>
    <script src='https://railsapp.com/dashboard/login.js'></script>
  </body>
</html>

Because <script> tags are allowed to be cross-origin (this is useful for CDNs), Tom's browser happily sends a GET request to railsapp.com, attaching his railsapp.com cookie. The RJS script is generated and returned to Tom, and his browser executes it. By stubbing out the necessary functions in the global scope, attacker.com can easily gain access to the string of HTML that is sent back:

<html>
  <body>
    <script>
      function $() {
        return {
          show: function() {
            return {
              html: function(str) {
                alert(str);
              }
            };
          }
        };
      }
    </script>
    <script src='http://railsapp.com/dashboard/login.js'></script>
  </body>
</html>

And now attacker.com can easily parse out Tom's CSRF auth token and start issuing malicious CSRF requests to railsapp.com. This means that attacker.com can submit any form in railsapp.com. The same technique can be used to leak other information besides auth token, including logged-in status, account name, etc.

As a pentester, how can I spot this bug while auditing a web app?

It is pretty easy to find this vulnerability. Click around a while in the web app and keep Web Inspector's Network tab open. Look for .js requests sent sometime after a page load. Any response to a .js request that includes private info (auth token, user ID, existence of a login session) can be "hijacked" using an exploit similar to the above PoC.

How can I fix this in my web app?

The fix prescribed by Rails is to go through your code and add request.xhr? checks to every controller action that uses RJS. This is annoying, and is a big pain if you have a large existing code base that needs patching. Since Metasploit Pro was affected by the vulnerability, we needed a patch quick. So I present our solution to the vulnerability - we now check all .js requests to ensure that the REFERER header is present and correct. The only downside here is that your app will break for users behind proxies that strip referers. Additionally, this patch will not work for you if you plan on serving cross-domain JavaScript (e.g. for a hosted JavaScript SDK). If you can stomach that sacrifice, here is a Rails initializer that fixes the security hole. Drop it in ui/config/initializers of your Rails app:

# This patch adds a before_filter to all controllers that prevents xdomain
# .js requests from being rendered successfully.

module RemoteJavascriptRefererCheck
  extend ActiveSupport::Concern

  included do
    require 'uri'
    before_filter :check_rjs_referer, :if => ->(controller) { controller.request.format.js? }
  end

  # prevent generated rjs scripts from being exfiltrated by remote sites
  # see http://homakov.blogspot.com/2013/11/rjs-leaking-vulnerability-in-multiple.html
  def check_rjs_referer
    referer_uri = begin
      URI.parse(request.env["HTTP_REFERER"])
    rescue URI::InvalidURIError
      nil
    end

    # if request comes from a cross domain document
    if referer_uri.blank? or
      (request.host.present? and referer_uri.host != request.host) or
      (request.port.present? and referer_uri.port != request.port)

      head :unauthorized
    end
  end
end

# shove the check into the base controller so it gets hit on every route
ApplicationController.class_eval do
  include RemoteJavascriptRefererCheck
end

And your server will now return a 500 error to any RJS request that does not contain the correct REFERER. A gist is available here, just download and place in $RAILS_ROOT/config/initializers.