Brian Slesinsky's Weblog
 
   

Sunday, 20 Aug 2006

Hello, Rails is Swiss Cheese

I investigated Ruby on Rails a little bit and found that it's easy to introduce security holes into a Rails application. (If you don't care about Ruby on Rails, you can skip this entry.)

Surprisingly enough, despite all the hype around Rails, nobody seems to have published a proper "hello world" script. (If you start by running "rails someapp", you have already gone wrong, because that generates 44 files. Hello world has to be a single file.) So, here's "hello world" in Rails as a CGI script:

#!/usr/bin/ruby
 
require 'action_controller'
 
class Hello < ActionController::Base
 
  def index
    @who = params.fetch('who', 'world')
    render :inline => PAGE 
  end
 
end
 
PAGE = <<END
<html>
<head>
  <title>Hello</title>
</head>
<body>
Hello, <%= h @who %>!
</body>
</html>
END
 
Hello.process_cgi(CGI.new, session_options=false)

Actually, that's slightly more than a hello world script, because I've added the ability to change "world" to some other name by passing in a "who" parameter.

I won't explain how it all works because the documentation for ActionController::Base covers it pretty well. The main difference between this script and a normal Rails application is that I've included the template directly in the script, using a here document. (Normally, Rails gets its templates from a template directory.)

The template is written in a language called eRuby, which can be used to generate any kind of file, not just HTML. The "h" appearing in the template is a function call that adds html escaping. (It's defined in ERB::Util as an abbreviation for html_escape.)

It's very important to call "h" for every template variable in an eRuby template, or there will be a serious security hole. If you leave it out, an attacker can embed JavaScript in the page and steal cookies from users who visit your website. Stealing cookies can be very bad when they're used for session tracking, because then the attacker can break into the user's account.

Having to religiously escape template variables everywhere is a design flaw common to many template languages. Escaping should happen by default, so that the template writer doesn't have to worry about it! But most people who write template languages don't bother with this, and it's a sad commentary on the state of web security today that dangerous template languages have become standards. (This includes JSP and ASP - it's not just Rails.)

Luckily, alternative template languages aren't hard to find. For Rails, the easiest alternative is the "Builder" template language. It's normally used for generating XML, but we can also use it for HTML. Here's "hello world" using Builder:

#!/usr/bin/ruby
 
require 'action_controller'
 
class Hello < ActionController::Base
 
  def index
    @who = params.fetch('who', 'world')
    render :inline => PAGE, :type => 'rxml', :content_type => 'text/html'
  end
 
end
 
PAGE = <<END
xml.html {
  xml.head {
    xml.title("Hello")
  }
  xml.body(
    "Hello, " + @who + "!"
  )
}
END
 
Hello.process_cgi(CGI.new, session_options=false)

Not only is this more secure, but you get a nice side benefit: pages generated this way will probably be well-formed XML, which makes it easy to parse them in unit tests.

However, it turns out that Rails comes with an old version of the Builder library that also has a security hole. The problem is that it escapes regular text, but not the values of attributes. To demonstrate how attributes values can be a security problem, I'll add a form where you can type in the name to be used in the greeting:

#!/usr/bin/ruby
 
require 'builder'
require 'action_controller'
 
class Hello < ActionController::Base
 
  def index
    @who = params.fetch('who', 'world')
    render :inline => PAGE, :type => 'rxml', :content_type => 'text/html'
  end
 
end
 
PAGE = <<END
xml.html {
  xml.head {
    xml.title "Hello"
  }
  xml.body {
    xml.p "Hello, " + @who + "!"
    xml.form {
       xml.p "Please enter your name:"
       xml.input(:type=>'text', :name=>'who', :value=>@who)
       xml.input(:type=>'submit', :value=>'Go')
    }
  }
}
END
 
Hello.process_cgi(CGI.new, session_options=false)

In this script, I've avoided the security hole by adding the require 'builder' statement. This causes Rails to use the version of Builder that I installed as a gem, instead of the old version that Rails comes with.

What this goes to show is that if you want security, you need to test it yourself. It's also important to write an end-to-end test to document what the hole was and make sure it stays fixed. Otherwise, you never know when upgrading a library will break things again.

For example, versions 1.1.0 through 1.1.5 of Rails had a more serious security hole. Someone who did their due dilligence using 1.0 would have been exposed by an upgrade. And given how long it took for that one to be discovered, it seems pretty likely that there are still more security bugs out there.