Insecurity is Ruby on Rails Best Practice

Update: This post is really really out of date. Please disregard most of what’s written here.

Ruby on Rails by default encourages developers to develop insecure web applications. While it’s certainly possible to develop secure sites using the Rails framework you need to be aware of the issues at hand and many technologies that make Rails a powerful easy to use platform will work against you.

Cross Site Request Forgery
CSRF is the new bad guy in web application security. Everyone has worked out how to protect their SQL database from malicious input, and RoR saves you from ever having to worry about this. Cross site scripting attacks are dying and the web community even managed to nip most JSON data leaks in the bud.

Cross Site Request Forgery is very simple. A malicious site asks the user’s browser to carry out an action on a site that the user has an active session on and the victim site carries out that action believing that the user intended that action to occur. In other words the problem arises when a web application relies purely on session cookies to authenticate requests.

Let’s look at a simple example. I want my site to add me to your 37Signals Highrise contact list.

37 Signals Highrise
Working out how to do this is pretty simple. I go to Highrise and take a look at the “Add a person” form in Firebug or using View Source. It’s a fairly straightforward form that submits to http://ianloic.highrisehq.com/people and has a bunch of fields. I can easily recreate that form on my own server. Since Highrise uses a different domain for each user I ask the user to enter their domain name. I use the same username I use everywhere else so it should be pretty easy to fool my user into giving me their Highrise domain name. Most other sites do not have unique per-user URLs.

The page is here and the source is here. Provided you’re logged in and you enter your highrise domain correctly clicking “Add me” will add my name to your contacts list because 37Signals’ servers have no idea that the request is not coming from their application.

It’s fairly straight-forward to modify a form like this to automatically submit on page load. It’s pretty easy to put it in an hidden IFRAME so the user doesn’t even know what’s going on.

By using a unique per-user URL the guys at 37 signals have made this exploit non-trivial. This is not the default behavior from the Ruby on Rails framework. By default action URLs are very predictable. For example if we take a look at the social bookmarking site Magnolia we see predictable URLs all over the place.

Magnolia
As soon as you log in to Magnolia you are presented with a form for adding a bookmark by typing in its URL. This is a fantastic user experience. Unfortunately for Magnolia’s users anyone that submits this form on their behalf can add bookmarks and the form’s action (http://ma.gnolia.com/bookmarks/quicksave) is the same for all users. A trivial page that adds a bookmark to a visitor’s Magnolia account would look like this (try it):

<html>
    <head><title>Ma.gnolia</title></head>
    <body onload="document.getElementById('f').submit()">
        <form id="f" method="post"
                action="http://ma.gnolia.com/bookmarks/quicksave">
            <input type="text" value="http://ian.mckellar.org/"
                name="url" id="url" type="hidden"></input>
        </form>
    </body>
</html>

But it gets even worse. Since by default Rails allows GET as well as POST submissions you can call an action from an IMG tag (try it):

<img src="http://ma.gnolia.com/bookmarks/quicksave?url=http://ian.mckellar.org/">

Magnolia is not unique in the behavior. Unless a site’s developer has gone out of their way to prevent it, this class of attacks will affect every Rails site. Most of the popular sites I’ve looked at exhibit some vulnerabilities.

Other Rails sites I’ve looked at attempt to do their input validation in JavaScript rather than in Ruby which leaves them open to JavaScript injection and hence XSS attacks. This is a far more serious attack that I can cover separately if there is interest.

Solutions
Easy Solutions
There aren’t any good easy solutions to this. A first step is to do referrer checking on every request and block GET requests in form actions. Simply checking the domain on the referrer may not be enough security if there’s a chance that HTML could be posted somewhere in the domain by an attacker the application would be vulnerable again.

Better Solutions
Ideally we want a shared secret between the HTML that contains the form and the rails code in the action. We don’t want this to be accessible to third parties so serving as JavaScript isn’t an option. The way other platforms like Drupal achieve this is by inserting a hidden form field into every form that’s generated that contains a secret token, either unique to the current user’s current session or (for the more paranoid) also unique to the action. The action then has to check that the hidden token is correct before allowing processing to continue.

This is a pain in the arse to write by hand.

What is really required at the Rails level is a of form API that can generate and consume forms securely. In Drupal all form HTML is generated and parsed by the framework. This allows application developers to protect themselves from XSRF without even knowing they are.

There’s a plugin called Secure Action but I’m not sure how well it works. The dependence on a static shared salt rather than a randomly generated secret in the user’s session concerns me. The way it puts the signature in the URL makes me nervous too. It’s better than nothing though.

32 replies on “Insecurity is Ruby on Rails Best Practice”

  1. Checking the HTTP Referrer Checking the HTTP Referrer is not a good idea. Firstly, there are valid reasons why the user could have the Referrer header feature turned off in their web browser. Secondly, an attacker can forge an HTTP Referrer header, although (probably) not via a CSRF.

  2. general problem in the web as michael and rick mentioned above, the whole REST approach in rails 1.2 solves the GET/POST unclarity problem.
    and the other thing you’re mentioning, about submitting forms from an other site, has nothing to do with rails. this is a general problem in the web and i think you’re suggestion with the secret token would be a way in the wrong direction (because it’s not RESTful). in my opinion this should be the job of the browser. the user should get a warning popup, or whatever, for every request he submits (except GETs) pointing to a different host than the current one.

  3. Trolling is any blogger’s best practice You are guilty of trolling the Rails community, but I do appreciate your exposing this important issue! Of course, I am guilty of feeding into your successful trolling by responding…

    I am not sure if building more restrictions into Rails and other app frameworks will solve the issue of common developer carelessness – with each security-related convenience, dangers such as performing state-changing operations with GETs will seem that much farther away and that much more likely to be imappropriately placed into a seemingly innoculous method such as “show”. However, security through convenience can be effectively encourage through “easier means to do the right thing” – such as how ActiveRecord enables the programmer to prevent sql injection attacks.

    In developing my first Rails app, I put logic in the controller methods to prevent state-changing GETs. I am somewhat negative about the new REST approach in 1.2 on first blush. This could be made a lot easier with a standard one-line before_filter to identify methods that can only process a POST request.

    Lastly, Luke’s suggestion is interesting and should be closely considered by browser developers: “in my opinion this should be the job of the browser. the user should get a warning popup, or whatever, for every request he submits (except GETs) pointing to a different host than the current one.”

    – Jamie

  4. Is it still a troll if I Is it still a troll if I have a valid point?

    The reason I blame Rails and not Rails application developers is that the framework seems to abstract away and hide so many ugly details.

    As for Luke’s suggestion of modifying browser behavior, I think that’s ridiculous. Perhaps I’m coming from the perspective of being a six year browser development veteran, but we’ve had the cookies web security model for ten years now. I don’t want to give it up and introduce yet-another security popup just so someone can have their “rest” URLs. Since it’s hard enough to get CSS implemented in popular browsers I wouldn’t hold my breath for this.

  5. interesting point You’re point is actually very interesting. And I think you’re right, this isn’t really the job of the webapplication but the browser’s one. HTTP supposed to be a stateless protocol and although there are some exceptions to this (sessions/cookies) we shouldn’t treat a webapplication like a normal application. Trying to control the workflow with some mysterious hashes sending over with every request is just not how the web should work. The web supposed to be as simple as possible and everything else should be reconsidered because there’s probably a better solution.

  6. Look, it’s rails behaving as drupal…. It’s prety easy to add the drupal behavior you so like to Rails. Here’s some real basic code that gets the job done with a new secret key for each controller, action and request. It should get you started. Add the before and after filters to the controllers as needed… you might also want to test for the request type and do other sexy things


    after_filter AddSecretKey, :only=>[:edit,:new]
    before_filter TestSecretKey, :only=>[:create,:update]

    class TestSecretKey
    def self.filter(controller)
    false if !controller.params.empty? && controller.params["secret"] != controller.session[:secret]
    end
    end
    class AddSecretKey
    def self.filter(controller)
    secret = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )[0..20]
    controller.session[:secret] = secret
    controller.response.body.sub! "</form>", "<input type=\"hidden\" name = \"secret\" value=\"#{secret}\"/></form>"
    end
    end

  7. not CSRF While I understand where you’re coming from with “‘referer’ isn’t secure”, the point of the CSRF attack is that the browser sends the session cookie back to the target site.

    In the trust relationship that exists between user browser cookie site, the browser is responsible for sending proper cookies and Referer headers. An exploit site would need to get the browser to send a spoofed Referer header along with the cross-site request. If the cookie site trusts the browser to do the right thing with the cookie, they can safely trust it to do the right thing with Referer as well.

  8. nothing like repeating again nothing like repeating again and again but it is googd for you tu understand: what on earth that has to do specific to rails?

  9. Nothing motivates like fear… W.r.t. good programming practices: you expose an important issue: make forms secure and
    validate the source of POST’ed entries.

    You expose a poor programming practice.

    Do a lot of web forms have this exposure, in your survey of the web?

  10. The Rails folks come out at night. Hey Ian, looks like you got the Rails folks up in arms. Pretty easy, really.

    Oh, and *wave*.

  11. Many many sites depend on Many many sites depend on being able to submit forms to other sites transparently. Many of them depend on cookies being set. Google authentication depends on this. Asking browser manufacturers to warn users about it is ridiculous. It’s up to web applications (and the frameworks they’re built on) to trust the cookies as much as is appropriate and not too much.

    It would be easy to make the same argument about SQL injection attacks or Cross Site Scripting too. Thankfully people have implemented frameworks that do a good job of taking care of these problems for us.

  12. Rails doesn’t provide the Rails doesn’t provide the protections that many other frameworks provide while hiding the scary details from developers. That’s bad.

  13. Many forms do, but many Many forms do, but many don’t. Services from major players like Google and Yahoo don’t have this problem. Digg doesn’t have this problem, del.icio.us doesn’t have this problem. Drupal sites don’t have this problem, but just about all the Rails sites I looked at have this problem.

    Basically any site that becomes popular enough to have people trying to exploit it will have to fix this problem. It would be better if the framework took care of the ugly details for us.

  14. Insecurity It is obvious that any framework is going to be as insecure as the person behind the keyboard. Drupal (php) is not necessarily insecure, but of course if you don’t know what goes on “behind the scenes” then you become a target.

    In my opinion the freshness of the framework provide new security challenges that indeed need to be address before going live with RoR.

    Jenn,
    Ruby on Rails Professional Developer

  15. django Django comes with a nice piece of middleware that takes the drupal approach: outgoing forms get an additional hidden field and incoming requests are checked for the token… This works automagically for most normal means of generating forms (ie, if you build forms in js libraries with dom calls or something, there’s no way to intercept the served html form…)

    See http://www.djangoproject.com/documentation/csrf/

  16. Any language This sort of exploits go for all languages.. PHP (especially when using register_globals) is just as bad. But both in PHP and Rails, this is easily preventable. Just don’t expect the language/framework to automagically do all the work for you.

  17. This blog post ranked pretty high in a Google search I did, so I thought I should say this in case someone unfamiliar with Rails finds it.

    The issues mentioned in the blog post have all been address in Rails. All Rails’ form helpers include a hidden field to protect against CSRF. You rarely have to even think about this issue anymore.

    Rails also now discourages allowing GET requests when they shouldn’t be allowed.

  18. This has nothing to do with rails.
    If you are updating info via GET you miss the whole point of REST.
    Articles like this are just a waste of time, and scare-mongering.
    Rails has protect against forgery built in, and you can always check the session against the account being updated anyway etc etc.

    Nothing insecure about Rails, or any other language, it is all about how you use it

  19. This post is obviously way out dated but even back in 2007 Rails had forgery protection built into the form helpers to prevent this.

    WARNING:

    THE DATA IN THIS POST IS OUTDATED, INACCURATE, AND SHOULD NOT BE RELIED ON.

Comments are closed.