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:

Sandboxing in ruby

October 26th, 2007

A few weeks ago, I decided to make a rails-based game. I wanted to bring the strength of ruby's metaprogramming into the game world, so I investigated sandboxing user uploaded code blocks. The only ruby sandbox was written by Why the Lucky Stiff, and you can find complete details on it here:

The Freaky Freaky Sandbox

The sandbox is an amazing hack on ruby's lookup tables to essentially allow a completely separate execution context with its completely own set of classes. The interesting part is how it interfaces with the outside world (the "Jungle"):

  • Classes can be copied in from the Jungle using Sandbox.import, and exist in both places with separate definitions. The sandbox automatically does this with simple essentially classes like String, Object, Hash, Array, etc.
  • Classes can be proxied in from the Jungle using Sandbox.ref. In this case, a proxy class is defined in the sandbox with exactly the same name as the outside class, but with only a two methods: const_missing & method_missing. When a method is called on the proxy, the sandbox is disabled and the actual method executes outside the sandbox. The result of the method is Marshalled into the sandbox, and it is enabled again.
  • Objects can be copied into the sandbox using Sandbox.set as long as they're defined there. This is accomplished by marshalling
  • Objects can be returned from the sandbox at the end of a Sandbox.eval call. This is accomplished by marshalling

Now you're ready to start writing your own applications using the sandbox. It's a pain to install in ruby 1.8.6, because it requires a small patch, but Why says that it works without patch in ruby 1.9.

I posted this originally on ELC's Blog

Tictactoe

October 15th, 2007
A demonstration of ruby sandboxing using some gems and plugins I wrote. You can upload your own TicTacToe algorithm and play against it! Read the rest of this entry
original design by gorotron ported by railsgrunt powered by mephisto