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.
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:
f.text_field(:foo).untaint instead of
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
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.
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:
The problem with the squeaky clean sanchez above is that there are so many methods to do. Ok, loops are easy:
Ok, maybe not so easy. Problems:
def, but I used
module_eval so I could interpolate strings inside the method definition.
alias_method_chain needs to delegate back by name.
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.)
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: