Picture of stu

Is your Rails app XSS safe?

  • Posted By Stuart Halloway on January 09, 2008

Act 1. Making Rails Safe From XSS.

All web frameworks need to protect against XSS. Since Aaron got SafeErb to work on Rails 2.0, it seemed liked a good time for a quick spike to get SafeErb integrated with a live project. Aaron and I picked a small project and started to work.

Act 2. Need a little yak hair...

Our little application had a few RESTful controllers and models, nothing complicated. The functional tests already hit every line of Erb in the views, so we should be able to just install the plugin, run the tests, and see what happens...

Boom. A few dozen errors. They fall into several categories:

  • Rails' FormBuilder methods return tainted strings, so you would have to say f.text_field(:foo).untaint instead of f.text_field :foo.
  • error_messages_for returns tainted strings. This one is a little tricky. Most validations don't regurgitate user input, but nothing stops a custom validation from doing so. I'll untaint this too, and if a validation exposes tainted data that is the validation's problem.
  • number_with_delimiter returns tainted strings. Rails should call untaint for me. But if you look at the code, number_with_delimiter doesn't do any escaping at all. Evil script in, evil script out.
  • controller.action_name (used in scaffolds) is tainted. Since that code won't survive til production anyway, we will just untaint it.
  • url_for and friends return a tainted string, but only sometimes. Sigh. The app is pretty small now, so we will just manually untaint the ones that blow up.

Act 3. Shaving the yak.

Well, Act 2 didn't go so well. That was a pretty small app, and the changes we had to make were numerous, ugly, and not DRY at all. Most developers probably wouldn't bother. But without this kind of systematic protection, we'd always be wondering how many XSS holes we have.

But wait, this is Ruby! I can metaprogram my way out of anything. We'll just duck punch Rails so that its methods call untaint at appropriate times. Something like this:

def text_field_with_untainting(*args,  blk)
  # TODO: some kind of escaping of args!
  text_field_without_untainting(*args,  blk).untaint
end
alias_method_chain method, :untainting

Act 4. Set the clipper guard to zero and smoke the hairs!

The problem with the squeaky clean sanchez above is that there are so many methods to do. Ok, loops are easy:

  [
    :date_select, 
    :datetime_select, 
    :password_field,
    :text_field       # add others as needed
  ].each do |method|
    module_eval(<<-END)
def #{method}_with_untainting(*args, &blk)
  #{method}_without_untainting(*args, &blk).untaint
end
END
    alias_method_chain method, :untainting
  end

Ok, maybe not so easy. Problems:

  1. I would prefer to use def, but I used module_eval so I could interpolate strings inside the method definition.
  2. Why do I need to interpolate strings? Well, alias_method_chain needs to delegate back by name.
  3. Of course, I don't want to use alias_method_chain anyway. I would rather use a block-based approach to wrap methods. But I can't because Ruby won't let you pass blocks to blocks.

And of course this approach still doesn't make the application XSS safe. It merely insists that the Rails helpers are XSS safe even though in many cases they aren't. (I am ok with this as a temporary expedient. I want to get my tests passing--fixing the Rails helpers is a project for another day.)

Act 5. Safety taint easy!

I simplified the first four acts to keep this posting short. It turns out that some of my Rails plugins were duck punching the same helper methods, in such a way that my punches never landed. So I had to double-duck punch them! This introduced some plugin load-order dependencies, giving me a chance to take advantage of Rails 2.0's configurable plugin load order.

Thoughts for the day:

  • I bet very few people are doing XSS security right in Rails. It is tricky, verbose, or both.
  • Ruby's open nature makes it easy for duck punches to get in each other's way. I still prefer this to the alternative, but things can get quite tricky.
  • There are a bunch of Rails helpers (and plugins) that need to change to work effectively with SafeErb. Aaron and I plan to pursue making these patches, if there is interest.
Comments
  1. Luke FranclJanuary 09, 2008 @ 11:40 PM

    Interesting. It sounds like SafeErb is kind of a pain to get working. And couldn’t f.text_field(:foo) return tainted data, if you were editing a record with contaminated data? Or are you stripping XSS from user input somehow?

    We are using Erubis for similar reasons. It can auto-escape HTML (or auto-strip tags which is interesting). It is kind of a pain to get working, like SafeErb, but worth it.

    http://railspikes.com/2007/12/10/rendering-erubis-and-rails-2-0

    I’m actually working on an an article about full-stack XSS protection. We’re using a combination of Erubis (which covers 90% of cases) plus acts_as_sanitized to sanitize HTML from models when they’re saved to cover the other 10% of cases when you need HTML in the output.

    Man, I wish Rails handled escaping HTML by default.

  2. Paul DoerwaldJanuary 14, 2008 @ 10:42 PM

    Fascinating. I’ve been trying to get this running on a project I’m working on, and it’s just one error after another.

    I think making SafeErb work in a Ruby on Rails application is essentially a hopeless goal. Your approach of untainting all the methods that are returning tainted strings seems to defeat the purpose — the strings are potentially still dangerous, as Luke indicates above. The problems with tainted strings run really deep. Rails needs to be untainting the strings, as appropriate, before the strings are ever bubbled up to the application. For instance, link_to returns a tainted string on account of the controller and action names which are read from the routes.rb file.

    Did you write the SafeErb plug-in? I really like it because it’s really powerful and helps you write super-secure code. It’s sad though, because it’s completely impractical due to how Rails is written.

    Paul.

  3. JustinJanuary 15, 2008 @ 02:39 AM

    Paul—

    Nope, we didn’t write SafeErb, though Aaron is a committer. And we agree—Rails needs to be untainting things deep inside the framework, which is why we are suggesting some patches to Rails itself to deal with some of these problems.