Is your Rails app XSS safe?

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.
Get In Touch