Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
RJS leaking vulnerability in multiple Rails applications (homakov.blogspot.com)
145 points by homakov on Nov 30, 2013 | hide | past | favorite | 67 comments


Here's a quick summary:

There's a Rails design pattern where you send an AJAX request to your controller, and it returns a bunch of Javascript that gets eval'd by the page you're on. This is used for say, infinite scroll. You'll click the 'load more' button, the server will render a partial containing your new records, and return something like:

    $('#itemsList').append('<li>My new item</li><li>another new item</li>')
So guess what? This is susceptible to XSS-ish attacks.

Let's imagine that instead of a simple list item, this request was returning some sensitive information or a CSRF token. (Egor points to various examples of this.) Normally, executing a GET request from an unauthorized domain will not be allowed because of CORS. This is what prevents me from instructing the users of my website to make an AJAX request to gmail.com, allowing me to see all of their emails.

But "wait", you say, "the content type of this request is application/javascript."

So open developer tools and type this: (using jQuery for conciseness)

    $.ajax({
      url: 'https://basecamp.com/[BASECAMP_PROJECT_ID]/progress.js', 
      dataType: 'jsonp'
    })
And boom, you have a bunch of sensitive information about your Basecamp project, accessible via JSONP request from any domain.


Isn't this solved by just requiring the CSRF token on any JS get requests? (In fact, isn't this just a cross-site request forgery with a different verb than we're used to?)

I know it's only generally checked on posts, but turning it on for any xhr calls seems like it would solve any potential data leakage.


Seems to me like this solution, or checking for `request.xhr?` are the best suggestions so far.


This one was mentioned by Homakov on Twitter I think. Though I would love to have it wrap the responder instead.


More importantly isn't this solved via authentication (basic auth).


basic/digest auth doesn't solve this if you've recently used the target site any more than cookies do, because your browser caches the authentication for some time and won't ask you again.

Also, very few web applications use basic auth, most use cookies.


it can be a solution, not ideal: proper non-xhr insertions on site.com will stop working. but for first step it's good enough.


Egor talked about it more in his original post, http://homakov.blogspot.com.au/2013/05/do-not-use-rjs-like-t...

He also linked to a post from six years ago[0] which had a good solution the problem at the time: pass data, not code (and process this data on the client side).

[0]: http://yehudakatz.com/2007/05/18/railsconf-talk-recap/


Your example makes it look like this is doable to any site that allows JSONP responses that contain sensitive data.

Is this RoR specific because RoR will always allow JSON responses to be turned into JSONP, or is there something else at work?

I'm suddenly pleased I've been holding off on doing JSONP as part of Django Slumber.


I just managed to confirm that a similar attack is possible against sites using DjangoRestFramework. I won't publish it as the site I used to test against is currently working on patching the vulnerability out.


If you believe you've found a security issue in Django REST framework I suggest raising emailing the security contact as listed here: http://django-rest-framework.org/#security

Having said that, it's worth pointing out that Django REST framework does not return JSONP by default, and although it does for historical reasons include a JSONP renderer, the documentation recommends the use of CORS instead.


I did email it pretty much straight away, but I guess it didn't get through.


>Is this RoR specific because RoR will always allow JSON responses to be turned into JSONP, or is there something else at work?

you can't turn any JSON into JSONP. No, JSONP here is result of using RJS templates. It has nothing to do with original JSONP


Except inasmuch as you're using a framework that allows any JSON data to also be requested as JSONP. The very first site I checked on this (which uses DjangoRestFramework) I was able to access email addresses from an attack page. This is purely due to DRF handling the JSONP for you without the devs really being aware of what was going on.


this is slightly different vulnerability (JSON->JSONP upgrade) but it's really severe. Good find!


Should people still use eval()? Am I missing something about the eval call in JS? if so, isn't that the first vulnerability anyway?


I think eval has some great use cases (maybe on a node.js server), just not what you are shipping off to the client.


Quick question, how are these files bypassing the authentication before_filter? I mean, a progress.js (Dont know what that is) probably belongs to a project, and therefore will hit that controller first, and the controller should make sure the file is not shipped to anyone but the authorized user. Am I missing something here?


The attack scenario here is a request from an authorized browser which is run by a hostile page on a different website (e.g., due to phishing attack). And since the request comes from an authorized browser, the cookies typically checked by an authentication before_filter will be present. In detail:

1) the attacker knows of an authorized user, already logged in (most likely). So, any request sent by that browser to the target site will pass authentication checks.

2) they trick that user into loading a web page under their control by some means (e.g., phishing attack).

3) that web page, under their control, includes a <script> tag with a src attribute referencing target .rjs. It loads the target .rjs Javascript into a gimmicked environment in which the private data is captured. Since the web page is under the attacker's control, same-origin rules allow that captured private data to be sent back to the attacker directly.

To counter this, the target site needs some way to verify that its own Javascript is making the request, not some hostile web site. Thus the talk of CSRF tokens and checking .xhr?. (The latter counts on the browser's same-origin rules to prevent an AJAX request from the hostile web site --- the same-origin rule blocks that, but in most current browsers, there's no way to prevent the attacker from tossing in a <script> tag.)


second part of your post looks vague. I'd say any GET-accessible .js template is leaking all data it has inside.

It's not precisely JSONP. It turns out to be JSONP, unintendedly. This is where it becomes vulnerable


You mentioned "creating a new record", which indicates a POST request.

I believe this only a vulnerability if the server responds to jsonp requests in GET requests.


You're right, will update now.


what I don't understand is: isn't the same information (say, a CSRF token) returned if the response is html (the AHAH pattern)? What makes rjs different?

EDIT: ah, got it, invokation via GET


The key to this exploit is that JavaScript that is designed to be fetched via XHR and then eval()d can be used by another site to "steal" data by linking to it in a script tag.

An obvious fix would be to send that JavaScript with a content type other than "application/javascript" or "text/javascript"... but considering some browsers execute JS sent with the wrong content type I wouldn't trust that to actually work.

A technique that definitely does work is to have the first line of the JS prevent execution in some way. When the code is fetched by XHR this line can be stripped out before calling eval(). This cannot be done by a malicious page that includes the code using a script tag.

I seem to remember seeing Google use "while(true);" for this exact purpose in the past.


>An obvious fix would be to send that JavaScript with a content type other than "application/javascript" or "text/javascript"... but considering some browsers execute JS sent with the wrong content type I wouldn't trust that to actually work.

I believe all browsers will try to execute. Sniffers

>I seem to remember seeing Google use "while(true);" for this exact purpose in the past.

Not this exact, but relevant. That one was JSON-leaking via redefining Array prototype.


But would this same solution work with this vulnerability? The server sets the first line of the response js to `while (true) ;` and the js handler that does the eval just deletes the first line like so:

     eval( responseJS.replace(/.*\n/, "") )


The exploit occurs when an evil third party site links to your script from a script tag - they can't modify the returned code before it is evaluated. Your own code running on the same domain (which fetches the code using XHR) can make the modification and hence execute the script.


Yes


I find it interesting that DHH dismissed this out-of-hand given Egor's history with Rails. In his tweets he highlights some pretty large Rails projects that impact a lot of users/sites.

I know a few of my own are affected by this.


He dismisses removal of it. He just wants to find a painless way to fix security concern. I don't know such way


It's clear that the author is frustrated though. Maybe it's just me but I feel like a framework's core team should treat security vulnerabilities (or even common design patterns that lead to them) as stop-the-world events until they are resolved.

It's not up to the issue reporter to come up with a fix that satisfies the core team.


Haha i practised pull request fixing zero days for a while. Ppl asked me to stop


I suppose this won't work for RJS, but for other similar JSONP vulnerabilities the correct approach is probably to strip out authentication from JSONP requests -- this could be done at the middleware layer or where authentication happens. Now JSONP would only leak data that you'd give to any other anonymous user.


Not perfect though. Intranet leaks


JSON and clientside templating, folks. It's a wee bit more work, but it's a lot more pure, and that purity pays off in multiple regards, including security.


I beg to differ. You can for instance return plain html and process it from a separate js file.


"wee" is subjective...

I have existing templates that exist server side. I don't want to recreate them client side. Rails JS responses to the rescue.. It's quite elegant and works well with the existing Rails workflow.


Nothing that relies on JS eval() is elegant, by any definition of the word.


Client side templating would be useful as a default feature in Rails, the framework already makes great use of partials. But to get client side templating to work you have to choose one of the new javascript frameworks and duplicate your partials into its' related js directory.

The Rails way has you transmitting html. That isn't great in cases where there may be thousands of entries, in this case the Rails way isn't the best.


I've been tinkering with a setup internally that uses a modified Hamlbars with Draper to generate serverside HTML and clientside templates from the same source template. It needs a bit of fleshing out, but the idea works.


This is sad, and this is being flagged too, so fast, seriously people? This is a genuine security issue which needs to be alerted to everyone out there. And Homakov has also gone so as far as to make some pull requests and he has been asked to stop (WTF?!). Really I see this more of like arrogance from the project maintainers' end.

I'm thankful that I don't depend/use on any of these features for my project though.

Thanks Homakov!!


This is why `request.xhr?` exists. It will be false when the action is called with JSONP.




Relevant, but out of date. It was a bug in browsers, not in the apps anyway


Always hated RJS. Too much toggling my brain.

Switching to jQuery made life so much easier. Once a feature is completed, I feel happy. With RJS, I was just glad that it was over. And, no, I do not want to touch it again :-)


How will jQuery solve this, if still using JSONP ?


Not sure I know what you mean.

I can say is that with jQuery you just get the JSON or HTML you need. And then you either process it or put it where its supposed to go. The server just creates the JSON or HTML.

An RJS file is a template for generating javascript on the server side with your data. Writing code with a tempting language is more of a pain than just writing that processes JSON or moves HTML.


I get redirected to a login page when attempting to access a spree /admin/products/new.js ... where's the leak or did I need to first have access to the users session?


someone with admin session must visit this page. Are you admin? you check it on demo spree installation


Quick fix for your Rails app: https://gist.github.com/javan/7725255


you have to be logged in for the request to work right?. if you are not logged in / don't have the cookie / session it won't work. so whats the problem? when your logged in you see all that stuff anyway why does it matter which flavor its in?

as long as your session is secured how will this work?


you make logged in user to visit specially crafted page, where you attack him with cross site <script> tag. and leak the data.


an easy fix, I think, would be to always wrap the returned code in a function that would check if the domain matches a predefined whitelist and just return out of the function if it doesn't. This can be very easily automated.


If you don't NEED to support GET requests with JSONP / JS to be evaled, then don't.

If you do need to, then the trick is to prefix what the script returns with something that will always throw a syntax error or discontinue evaluation, the idea being that your script (same domain) can cut off the prefix and then evaluate the JS, but the attacker relying on a <script> tag cannot.

  for(;;);
http://blackbe.lt/safely-handling-json-hijacking-prevention-...


It's not an option. Attacker can set getter on location.domain to bypass thath "JS protection".


location.__defineGetter__('host',function(){return 'google.com'}) undefined location.host "google.com"


That's a good point, haven't thought of that. So the idea sucks...


Not a fix at all. The client can examine the returned code.


I was pretty sure that if its comming from a different domain that this is not the case.

For example:

If we have legitsite.com/json-only-with-sensitive-data.js and we load that by adding a script tag to attackersite.com then attackersite.com cannot get the contents of what the legit site returned.

The problem is when you either have JSONP (for which the attacker site could add a callback in the request to execute) or if you return js code that is supposed to run like in this blog post where RoR returns code like this:

    $('#someid').html('sensitive data');
    ...
In this case it's just as bad as having JSONP because the attacker site can create a <div id='someid'></div> then add the script tag then onLoad of the script tag inspect the contents of the div.


actually what I had in mind was doing something like:

(function () {

    if (location.hostname !== 'mydomain.com')

        return;

    // the real content here, e.g:
    $('#someid').html('sensitive data');
})();

Makes any sense?


proved above, can be bypassed


Can you show an example how? Not 100% sure, but I believe all major browsers will block the direct access to the content of an external JS file from a different domain? You get just an empty string if accessing it from the DOM (at least in chrome)


RJS was a great technology in 2006. But now RJS is a dead-end and its time to let it go.

Replace RJS with jQuery (and the like) and focus on solving modern problems like concurrency and meteor-style push.


RJS was removed in Rails 3.1, IIRC. It hasn't been considered best practice for some time.

That said, an article has been on the front page all day about not using shiny new tech by an author that recommends the use of Rails 2.3 and RJS today, soooo...


In my post RJS implies JS responders. RJS doesnt exist anymore


Yeah, you might want to clarify that this can affect anyone who renders JS with erb or whatever, as it reads like only RJS (RIP) is affected.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: