strapyourself.in and flouri.sh
Safely exposing your app to a ruby Sandbox
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
Boardreturns aMovethenBoardWrapperreturns aMoveWrapper) - Wrap the contents of arrays and hashes (same as above, but will work with arrays of
Move, and Hashes containingMove) - 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
Sandboxing in ruby
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 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.setas long as they're defined there. This is accomplished by marshalling - Objects can be returned from the sandbox at the end of a
Sandbox.evalcall. 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