sandboxed_erb

This is a gem that allows you to run an ERB template in a sandbox so it is safe to expose these templates to your customers to allow them to customise your application (or any other use you can think of).

This has been inspired by github.com/tario/shikashi, a ruby sandbox which uses the evalhook gem to intercept and rewrite every ruby call to go through an access check at runtime. It was originally designed to run in the shikashi sandbox, but was found to be too slow as every call was being intercepted and analysed at runtime. Because a templating language does not need everything ruby offers, i have limited the allowed synax to a safer subset at compile time to reduce the number of runtime checks required.

To install

sudo gem install sandboxed_erb

How It Works

The code does not run in a ‘sandbox’ like javascript, it is actually processed into ‘safe’ code then run using the normal ruby intepreter.

  1. The template is first processed by the ERB compiler to produce valid ruby code.

  2. The generated erb code is then processed to check that if conforms to the ‘whitelist’of allowed syntax.

  3. Every invokation on an object is converted from some_object.some_method(arg1,argn) to some_object._sbm(:some_method,arg1,argn).

  4. Run using ruby intepreter.

  5. The _sbm method checks at runtime that the method is allowed as per rules defined by ‘Module.sandboxed_methods’ and ‘Module.not_sandboxed_methods’

Why Is The Code Safe?

  • I use a ‘white list’ of allowed syntax, so if I’ve missed something it will be denied.

  • I dont allow the defining of any classes, methods or modules.

  • You cannot access global variables or constants (or define them)

  • Every call is routed through the _sbm method, so if an non-safe object is somehow called, it wont have the _sbm method, so it will error.

_sbm (SandBoxed Method)

Heres an example of the generated code after is has been processed:

Source:

email_address = user. + "@" + user.domain(:local_domain)

Processed Code

email_address = user._sbm(:login)._sbm(:+,"@")._sbm(:+,user._sbm(:domain, :local_domain))

_smb will check that the symbol of the function to call (:login and :domain) has been explicitly allowed for that object using the sandboxed_methods module function (example below). If it has been allowed, it is assumed that the method getting called is safe (developers responsibility!) and that it can handle the arguments (which should be safe because you cannot define methods etc in a sandbox).

Optimisations

  • The ERB template generates many _erbout.concat calls, these are not routed through _sbm.

  • to_s is called heaps, it is assumed .to_s is safe to all (without arguments) an any object.

Benchmark Results

Taken from profile/vs_liquid.rb

                         user     system      total        real
erb template         0.030000   0.000000   0.030000 (  0.024003)
sandboxed template   0.100000   0.000000   0.100000 (  0.100563)
liquid template      3.040000   0.020000   3.060000 (  3.116854)

Examples

Calling some sandboxed methods

You can define a class and specify what methods are safe to use from within the sandbox using the sandboxed_methods function.

#Define a class we want accessable from the sandbox
class SandboxedClass

  sandboxed_methods :method_i_can_call

  def method_i_can_call(arg1)
    "{arg1} passed in"
  end

  def private_method(arg1)
    "You cannot call this from the sandbox"
  end
end

#a basic template that calls 'method_i_can_call' on an instance of SandboxedClass passed in below
str_template = "test = <%=sandboxed_class.method_i_can_call('some value')%>"

template = SandboxedErb::Template.new
#compile the template so it can get run multiple times
template.compile(str_template)
#run the template, passing in an instance of SandboxedClass as a variable called 'sandboxed_class'
result = template.run(nil, {:sandboxed_class=>SandboxedClass.new})

#result = "test = some value passed in"

Mixins / Helper functions

To add helper functions to the template, you can define the helper function mixins when the template is instantiated.

#define helper functions in a module
module MixinTest
  def test_mixin_method 
    "TEST #{@controller.some_value}"  #@controller is a 'context' object 
  end
end

#define a 'context' object that the helper function will have access to
class FauxController
  def some_value
    "ABC"  
  end
end

faux_controller = FauxController.new

str_template = "mixin = <%= test_mixin_method %>"
#declare the template, passing in the MixinTest module so its test_mixin_method will be available as a helper function
template = SandboxedErb::Template.new([MixinTest])
template.compile(str_template)
#pass the FauxController object as a context object called @controller (the keys are converted to member variables)
result = template.run({:controller=>faux_controller}, {}) 

#result =  "mixin = TEST ABC"

Contributing to sandboxed_erb

  • Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet

  • Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it

  • Fork the project

  • Start a feature/bugfix branch

  • Commit and push until you are happy with your contribution

  • Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.

  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Copyright © 2011 Mark Pentand. See LICENSE.txt for further details.