Sunday, 27 January 2013

XML-YAML-parsing security fix for older versions of rails

Earlier I mentioned the Serious Rails vulnerability that affects all versions of Rails for the last six years.

A fix has been put into the latest versions of Rails 2 and 3. but it requires you to upgrade to the latest version.

If you have an older version of rails and can't upgrade for various reasons (eg we are still stuck on v 2.3.2 due to some legacy code), there's a better fix for the *link* xml parsing error than the workarounds on offer (which tend to switch off your ability to parse XML).

The fix that we've done requires that you use bundler, though you could equally-well freeze rails into vendor/gems and make the same patch there. We chose the bundler/github approach because it reduces the size of our repository.

Step 1: fork a copy of rails for yourself

  1. fork rails
  2. git clone it into a local directory.
  3. checkout the *tag* that corresponds with the version you are on (eg for us: v2.3.2.1) - you can see what tags there are by running: "git tag -l"
  4. Don't worry about the big scary message it gives you about a detached HEAD - that just means you've got a specific commit checked out instead of a branch.
  5. create a branch for yourself eg for us: git branch v2.3.2_xmlfixes
  6. checkout that branch eg git checkout v2.3.2_xmlfixes
  7. push that to your repo on github with eg: git push -u origin v2.3.2_xmlfixes

Now you have a forked copy of the rails repo with a branch at the rails-version you are using that you can refer to in your Gemfile.

Step 2: actually apply the patch...

cherry-pick this commit (which if you look at github is is the one from v 2.3.15 that fixes this very error). Using:

git cherry-pick 70adb9613e4a40c5645c99da37

Note: You are likely to get conflicts with the CHANGELOG - you can keep them or just throw them away as you wish (it's just the changelog which describes the latest changes).

commit and push to your repository.

Now you have a patched version of rails in your git repository.

Step 3: update your Gemfile

Your Gemfile is likely to have a line that includes rails such as:

gem 'rails', '2.3.2'

You need to update that line to point at *your* git repository and your new branch.

The following *should* Just Work:

gem 'rails', '2.3.2', :branch => "v2.3.2_xmlfixes", 
:git =>l 'git://github.com/<your_git_repo>/rails.git'

To find the git url, you can go to your forked copy of git, look near the top of the page where it has a text-box with a git-link. Make sure you click on the "Git Read-only" button, and copy what's in the box on the right.

The branch to use is whatever you named your branch in step 5 above.

You should now be able to run bundle install to regenerate your copy of rails - and it will pull the details from your forked-and-patched copy

Troubleshooting

Unfortunately, when I used the above, it bundled correctly, but any attempt to spin up the server just caused the following error:

./script/../config/boot.rb:61:in `require': no such file to load -- initializer (LoadError)
 from ./script/../config/boot.rb:61:in `load_initializer'
 from ./script/../config/boot.rb:117:in `run'
 from ./script/../config/boot.rb:17:in `boot!'
 from ./script/../config/boot.rb:130
 from script/server:2:in `require'
 from script/server:2

Luckily the answer is in the StackOverflow question: how to use a branch in a fork of rails in a project with bundler.

First, you need to add .gemspec files into your patched copy of rails. If you're forking 2.3.10, you can copy the gemspec files from the commit Adding .gemspec files for all gems in the 2-3-stable version of rails created by the author of the above stackoverflow issue.

Otherwise you'll need ones correct for your own version. The commit above talks about generating them from the associated Rakefiles. I created them by copying the gemspec files listed in the commit above, and then just copying over the spec = Gem::Specification block with the equivalent block that is in the Rakefiles.

eg for actionpack.gemspec, I copied the actionpack.gemspec from the commit into the rails/actionpack directory in my forked copy of rails. Then I opened up rails/actionpack/Rakefile and copied the whole block of code that begins with spec = Gem::Specification into the actionpack.gemspec file, deleting the previous block from that file first.

You will know if you got the gem-dependencies wrong if you get an error like the following:

Bundler could not find compatible versions for gem "activesupport":
  In Gemfile:
    actionpack (>= 0) ruby depends on
      activesupport (= 2.3.10) ruby

    activesupport (2.3.2)

Now you have generated the gemspecs, add them to your forked copy of Rails, commit them and push the commit to your github repo.

Then you can put the following in your Gemfile:

:git => 'git://github.com/<your_git_repo>/rails.git', :branch => "v2.3.2_xmlfixes" do
  # Note: load-order is essential for dependencies
  gem 'activesupport',  :branch => "v2.3.2_xmlfixes" # this must go first
  gem 'actionpack',     :branch => "v2.3.2_xmlfixes" # this must go second
  gem 'actionmailer',   :branch => "v2.3.2_xmlfixes"
  gem 'activerecord',   :branch => "v2.3.2_xmlfixes"
  gem 'activeresource', :branch => "v2.3.2_xmlfixes"
  gem 'rails',          :branch => "v2.3.2_xmlfixes" # this must go last
end 

Note: make sure the gems are in the order above, with rails last - otherwise you'll get something like:

Could not find gem 'activesupport (= 2.3.10) ruby', 
   which is required by gem 'activerecord (>= 0) ruby', in any of the sources.

Also note: you *must* explicitly set the branch on the git-repo line and *also8 on all the gem-lines (and they must match) otherwise bundle install will work fine, but if you try anything else you'll get the infuriating error:

git://github.com/<your_git_repo>/rails.git (at v2.3.2_xmlfixes) is not checked out. Please run `bundle install`

Finally

you should now be able to run bundle update and bundle install and it should now work.

This has been tricky to explain, and the steps are complex - if something's not clear, let me know and I'll try and make it more plain.

3 comments:

AkitaOnRails said...

If you don't need your controllers to parse YAML inside XML you can disable it completely by adding an initializer file such as config/initializers/disable_parsers.rb with:

ActionController::Base.param_parsers.delete(Mime::XML)
ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING.delete('symbol')
ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING.delete('yaml')

You should also check if you are using the multi_xml gem and make sure it's using at least version 0.5.2.

Taryn said...

Thanks for that - looks like it might also do the trick, with less hassle of forking et al.

Still, it's been a good learning experience, and it's still good to know we have the patch that was actually added to 2.3.15 for this specific problem.

Taryn said...

Just so you know, the comment above actually switches off all xml-parsing... which was not what we needed, unfortunately, as doing so broke our XML API interface.

What we needed was a solution that allowed us to continue to use an XML interface, without allowing symbol or yaml parsing (which we don't do).

Removing the first of the above three lines helped us, though, and we have kept that *along* with our patched version of rails.