strapyourself.in and flouri.sh

Safely exposing your app to a ruby Sandbox

October 27th, 2007

Creating wrapper classes for the sandbox

When creating my sandboxed game of Tictactoe (where a user can upload a new algorithm and play tictactoe against it), I wanted to expose only a small part of my application to user uploaded code. In the follow code, for example, I would want to provide user access to only a few methods of the Board class:

class Board < ActiveRecord::Base
  has_many :moves
  belongs_to :algorithm_x, :class_name => "Algorithm", :foreign_key => "algorithm_x_id"
  belongs_to :algorithm_o, :class_name => "Algorithm", :foreign_key => "algorithm_o_id"

  def make_move!(x, y)...
  def move_matrix...
  def log_info(msg)...
  def winner...
  def game_over...
  def make_computer_move!...
  def human_turn?...
end

If I want to allow the user's code to access make_move, moves, move_matrix, log_info only, I'd create a wrapper class as follows:

class BoardWrapper
  def initialize(board); @board = board; end
  def make_move(x,y); @board.make_move(x,y); end
  def moves; @board.moves.collect {|m| MoveWrapper.new(m) }; end
  def move_matrix; @board.move_matrix; end
  def log_info(msg); @board.log_info(msg); end
end

acts_as_wrapped_class

This is pretty cumbersome to build, so I built acts_as_wrapped_class to make creating these wrappers easy. It does the following:

  • Automatically generate a wrapper class for each class marked as acts_as_wrapped_class
  • Dispatch methods that match (or don't match) a safelist or blacklist
  • Finds appropriate wrappers for return results (meaning if Board returns a Move then BoardWrapper returns a MoveWrapper)
  • Wrap the contents of arrays and hashes (same as above, but will work with arrays of Move, and Hashes containing Move)
  • Dispatch ===, hash, <=> methods directly to the wrapped objects. Compare two wrappers objects and get the same results as the two wrapped objects.

The above example is much shorter when written with acts_as_wrapped_class:

class Board < ActiveRecord::Base
  acts_as_wrapped_class :methods => [:moves, :make_move!, :move_matrix, :log_info]

  def make_move!(x, y)...
  def move_matrix...
  ...
end

class Move < ActiveRecord::Base
  belongs_to :board
    
  acts_as_wrapped_class :methods => [:x_pos, :y_pos, :is_x, :created_at]
end

Simple executing acts_as_wrapped_class inside the definition of Board automatically defines the BoardWrapper class with checks on which methods are called. This is accomplished through undefining all the methods of BoardWrapper and defining a method_missing which checks the safelist/blacklist before dispatching the method call.

Try to access winner on a BoardWrapper and it will throw an exception, because :winner isn't on the list of approved classes. Of course, you can call wrapper._wrapped_class and get access to the original Board object, but if you've set up your sandbox correctly, the class Board will not even be defined in the sandbox and will raise an exception.

View the RDOC for acts_as_wrapped_class for more detail.

acts_as_runnable_code

In order to make sandboxing user code even easier, I created another gem: acts_as_runnable_code. This gem helps you with the creation of the sandbox, the referencing of the wrapper classes, and automatic wrapping/unwrapping of data as it flows in and out of the sandbox. It assumes the following about your application

  • you have objects that store user uploaded code in them
  • you want to use your classes in the sandbox with reduced functionality provided by acts_as_wrapped_class
  • you want to evaluate an instance of user uploaded code within the context of some instance of a wrapped class

When writing tictactoe, I created an Algorithm model which stored user uploaded code in a database TEXT field. I also wanted to evaluate that code using the binding of the Board object on which the game was being played (meaning the user code looks like "make_move!(1,1)" rather than "@board.make_move(1,1)").

class Algorithm < ActiveRecord::Base
  acts_as_runnable_code
end

@board = Board.find(id)
@board.algorithm_x.run_code(@board, :timeout => 1.0)

View the RDOC for acts_as_runnable_code gem.

To see tictactoe in action, create your own algorithm, and test the safety of the sandbox (scary!) visit tictactoe.mapleton.net

I originally posted this on ELC's blog:

Sorry, comments are closed for this article.

original design by gorotron ported by railsgrunt powered by mephisto