1. Skip to navigation
  2. Skip to content

The ELC Community Blog

A knowledge exchange on Ruby on Rails and Agile Development


Safely exposing your app to a ruby Sandbox

by stevend on October 23, 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

   1  Board
class:

   1  class Board < ActiveRecord::Base
   2    has_many :moves
   3    belongs_to :algorithm_x, :class_name => "Algorithm", :foreign_key => "algorithm_x_id"
   4    belongs_to :algorithm_o, :class_name => "Algorithm", :foreign_key => "algorithm_o_id"
   5  
   6    def make_move!(x, y)...
   7    def move_matrix...
   8    def log_info(msg)...
   9    def winner...
  10    def game_over...
  11    def make_computer_move!...
  12    def human_turn?...
  13  end

If I want to allow the user's code to access

   1  make_move, moves, move_matrix, log_info
only, I'd create a wrapper class as follows:

   1  class BoardWrapper
   2    def initialize(board); @board = board; end
   3    def make_move(x,y); @board.make_move(x,y); end
   4    def moves; @board.moves.collect {|m| MoveWrapper.new(m) }; end
   5    def move_matrix; @board.move_matrix; end
   6    def log_info(msg); @board.log_info(msg); end
   7  end

acts_as_wrapped_class

This is pretty cumbersome to build, so I built

   1  acts_as_wrapped_class
to make creating these wrappers easy. It does the following:

  • Automatically generate a wrapper class for each class marked as
       1  acts_as_wrapped_class
    
  • Dispatch methods that match (or don't match) a safelist or blacklist
  • Finds appropriate wrappers for return results (meaning if
       1  Board
    
    returns a
       1  Move
    
    then
       1  BoardWrapper
    
    returns a
       1  MoveWrapper
    
    )
  • Wrap the contents of arrays and hashes (same as above, but will work with arrays of
       1  Move
    
    , and Hashes containing
       1  Move
    
    )
  • Dispatch
       1  ===, hash, &lt;=&gt;
    
    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:

   1  class Board < ActiveRecord::Base
   2    acts_as_wrapped_class :methods => [:moves, :make_move!, :move_matrix, :log_info]
   3  
   4    def make_move!(x, y)...
   5    def move_matrix...
   6    ...
   7  end
   8  
   9  class Move < ActiveRecord::Base
  10    belongs_to :board
  11      
  12    acts_as_wrapped_class :methods => [:x_pos, :y_pos, :is_x, :created_at]
  13  end

Simple executing acts_as_wrapped_class inside the definition of

   1  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

   1  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
   1  wrapper._wrapped_class
and get access to the original Board object, but if you've set up your sandbox correctly, the class
   1  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

   1  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)").

   1  class Algorithm < ActiveRecord::Base
   2    acts_as_runnable_code
   3  end
   4  
   5  @board = Board.find(id)
   6  @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

Comments

Eric Anderson at 1:22 PM on October 23 2007

I haven’t taken a close look at this post yet but it’s protection pretty much advisory in Ruby and not enforced? For example couldn’t I use instance_variable_get to get the underlying object and then call on that object directly? I guess you could override instance_varaible_get to look at the caller or something.

David Stevenson at 11:15 PM on October 23 2007

Eric, Of course you can get access to the underlying object outside of the sandbox, but not inside. While instance_variable_get is actually undefined for Wrapper objects, I provide a method ._wrapped_object to access the contained object.

This isn’t a concern inside the sandbox, because we control which classes are defined inside in the sandbox. Only Wrapper classes and core datatypes are defined there (this automatically done by the acts_as_runnable_code gem, nothing about the sandbox prevents you from defining whatever you want in there).

Hence, when the malicious code calls ._wrapped_object, the unwrapped object is returned (marshaled) into the sandbox… where it FAILS to unmarshal because that class isn’t even defined there!

Add a comment


home | services | Ruby on Rails Development | code | blog | company