Safely Parsing Parameters from a Rails Log

The production logs for a Ruby on Rails application include the parameters that were part of each request. The parameters are displayed in the format of an inspected Ruby hash. For example:

Started POST "/blog/posts" for 192.168.1.65 at 2012-06-04 06:24:34 -0400
  Processing by PostController#create as HTML
  Parameters: {"post"=>{"title"=>"Great Post", "body"=>"This is a blog post"}, 
               "commit"=>"Submit"}
Redirected to http://localhost:3000/blog/posts
Completed 302 Found in 104ms

If you need to parse these parameters from a log file for any reason, it is possible to do so in a simple and safe way using a blank slate object and Ruby’s $SAFE levels.

If the parameters were output as JSON then parsing them would be a trivial problem. But since that is not the case right now, the only choice is to use Ruby’s eval functionality since the text is valid Ruby code. As most people are aware, evaling data that has been submitted from the web is not a smart thing to do. But with a few simple precautions it can be done in a very safe manner.

This StackOverflow response outlines what needs to be done. The idea is that you want to eval the input within the context of a blank slate object that has only the bare minimum of methods on it. In addition you want to set the $SAFE level to 4 to create a secure sandbox for evaluating the input. Ruby’s Safe Levels are discussed in the Locking Ruby in the Safe chapter of the Programming Ruby book.

Ruby 1.9 comes with a blank slate object called BasicObject, and you can create your own in Ruby 1.8 quite easily. This Rails Tip uses the following code that will work in both 1.8 and 1.9:

class BasicObject
  instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval/ }
end unless defined?(BasicObject)

As the StackOverflow response describes, if you increase the $SAFE level in a Proc, it will go back to normal when the Proc ends. In order to work with both 1.8 and 1.9 I had to slightly modify the code from the StackOverflow response to no longer eval using the blank slate object’s binding, but to instead just instance_eval the input directly as the binding method does not exist on the 1.9 BasicObject class.

def parse_parameters_hash(str)
  proc do
    $SAFE = 4
    BasicObject.new.instance_eval(str)
  end.call
end

When the $SAFE level is set to 4 it is not possible to read from or write to the filesystem, which means that external Ruby files can’t be required or loaded. If you wait until after the parameters have been evaluated to load the Rails environment then it is not possible for malicious code to use any of your application’s classes.

params = parse_parameters_hash str
require 'config/environment'

# do stuff with the params

Combining a blank slate object and a $SAFE level of 4 like this allows you to eval Ruby code, like a hash of parameters, without compromising the security of your application.