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 class: 1 Board
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 only, I'd create a wrapper class as follows: 1 make_move, moves, move_matrix, log_info
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 to make creating these wrappers easy. It does the following: 1 acts_as_wrapped_class
- 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
returns a1 Board
then1 Move
returns a1 BoardWrapper
)1 MoveWrapper
- Wrap the contents of arrays and hashes (same as above, but will work with arrays of
, and Hashes containing1 Move
)1 Move
- Dispatch
methods directly to the wrapped objects. Compare two wrappers objects and get the same results as the two wrapped objects.1 ===, hash, <=>
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 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. 1 Board
Try to access 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 winner
and get access to the original Board object, but if you've set up your sandbox correctly, the class 1 wrapper._wrapped_class
will not even be defined in the sandbox and will raise an exception. 1 Board
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 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 Board
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
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.
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!