sandboxed_erb 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +127 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/example/controller.rb +11 -0
- data/example/example.rb +87 -0
- data/example/listing.sbhtml +26 -0
- data/example/note.rb +17 -0
- data/example/users.rb +55 -0
- data/example/view_notes.sbhtml +9 -0
- data/lib/sandboxed_erb.rb +45 -0
- data/lib/sandboxed_erb/sandbox_methods.rb +93 -0
- data/lib/sandboxed_erb/system_mixins.rb +37 -0
- data/lib/sandboxed_erb/template.rb +224 -0
- data/lib/sandboxed_erb/tree_processor.rb +215 -0
- data/profile/vs_erb.rb +77 -0
- data/profile/vs_liquid.rb +95 -0
- data/sandboxed_erb.gemspec +79 -0
- data/test/helper.rb +18 -0
- data/test/test_compile_errors.rb +142 -0
- data/test/test_error_handling.rb +59 -0
- data/test/test_sandboxed_erb.rb +230 -0
- data/test/test_valid_templates.rb +170 -0
- metadata +181 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
gem "partialruby", ">= 0.2.0"
|
6
|
+
gem "ruby_parser", ">= 2.0.6"
|
7
|
+
|
8
|
+
# Add dependencies to develop your gem here.
|
9
|
+
# Include everything needed to run rake, tests, features, etc.
|
10
|
+
group :development do
|
11
|
+
gem "shoulda", ">= 0"
|
12
|
+
gem "bundler", "~> 1.0.0"
|
13
|
+
gem "jeweler", "~> 1.6.1"
|
14
|
+
gem "rcov", ">= 0"
|
15
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Mark Pentland
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
= sandboxed_erb
|
2
|
+
|
3
|
+
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).
|
4
|
+
|
5
|
+
This has been inspired by http://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.
|
6
|
+
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,
|
7
|
+
i have limited the allowed synax to a safer subset at compile time to reduce the number of runtime checks required.
|
8
|
+
|
9
|
+
|
10
|
+
== How It Works
|
11
|
+
|
12
|
+
The code does not run in a 'sandbox' like javascript, it is actually processed into 'safe' code then run using the normal ruby intepreter.
|
13
|
+
|
14
|
+
1. The template is first processed by the ERB compiler to produce valid ruby code.
|
15
|
+
2. The generated erb code is then processed to check that if conforms to the 'whitelist'of allowed syntax.
|
16
|
+
3. Every invokation on an object is converted from some_object.some_method(arg1,argn) to some_object._sbm(:some_method,arg1,argn).
|
17
|
+
4. Run using ruby intepreter.
|
18
|
+
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'
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
== Why Is The Code Safe?
|
23
|
+
* I use a 'white list' of allowed syntax, so if I've missed something it will be denied.
|
24
|
+
* I dont allow the defining of any classes, methods or modules.
|
25
|
+
* You cannot access global variables or constants (or define them)
|
26
|
+
* 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.
|
27
|
+
|
28
|
+
== _sbm (SandBoxed Method)
|
29
|
+
Heres an example of the generated code after is has been processed:
|
30
|
+
|
31
|
+
Source:
|
32
|
+
email_address = user.login + "@" + user.domain(:local_domain)
|
33
|
+
|
34
|
+
Processed Code
|
35
|
+
email_address = user._sbm(:login)._sbm(:+,"@")._sbm(:+,user._sbm(:domain, :local_domain))
|
36
|
+
|
37
|
+
_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).
|
38
|
+
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).
|
39
|
+
|
40
|
+
|
41
|
+
== Optimisations
|
42
|
+
* The ERB template generates many _erbout.concat calls, these are not routed through _sbm.
|
43
|
+
* to_s is called heaps, it is assumed .to_s is safe to all (without arguments) an any object.
|
44
|
+
|
45
|
+
== Benchmark Results
|
46
|
+
Taken from profile/vs_liquid.rb
|
47
|
+
user system total real
|
48
|
+
erb template 0.030000 0.000000 0.030000 ( 0.024003)
|
49
|
+
sandboxed template 0.100000 0.000000 0.100000 ( 0.100563)
|
50
|
+
liquid template 3.040000 0.020000 3.060000 ( 3.116854)
|
51
|
+
|
52
|
+
|
53
|
+
== Examples
|
54
|
+
|
55
|
+
=== Calling some sandboxed methods
|
56
|
+
You can define a class and specify what methods are safe to use from within the sandbox using the sandboxed_methods function.
|
57
|
+
|
58
|
+
#Define a class we want accessable from the sandbox
|
59
|
+
class SandboxedClass
|
60
|
+
|
61
|
+
sandboxed_methods :method_i_can_call
|
62
|
+
|
63
|
+
def method_i_can_call(arg1)
|
64
|
+
"{arg1} passed in"
|
65
|
+
end
|
66
|
+
|
67
|
+
def private_method(arg1)
|
68
|
+
"You cannot call this from the sandbox"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
#a basic template that calls 'method_i_can_call' on an instance of SandboxedClass passed in below
|
73
|
+
str_template = "test = <%=sandboxed_class.method_i_can_call('some value')%>"
|
74
|
+
|
75
|
+
template = SandboxedErb::Template.new
|
76
|
+
#compile the template so it can get run multiple times
|
77
|
+
template.compile(str_template)
|
78
|
+
#run the template, passing in an instance of SandboxedClass as a variable called 'sandboxed_class'
|
79
|
+
result = template.run(nil, {:sandboxed_class=>SandboxedClass.new})
|
80
|
+
|
81
|
+
#result = "test = some value passed in"
|
82
|
+
|
83
|
+
|
84
|
+
=== Mixins / Helper functions
|
85
|
+
To add helper functions to the template, you can define the helper function mixins when the template is instantiated.
|
86
|
+
|
87
|
+
#define helper functions in a module
|
88
|
+
module MixinTest
|
89
|
+
def test_mixin_method
|
90
|
+
"TEST #{@controller.some_value}" #@controller is a 'context' object
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#define a 'context' object that the helper function will have access to
|
95
|
+
class FauxController
|
96
|
+
def some_value
|
97
|
+
"ABC"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
faux_controller = FauxController.new
|
102
|
+
|
103
|
+
str_template = "mixin = <%= test_mixin_method %>"
|
104
|
+
#declare the template, passing in the MixinTest module so its test_mixin_method will be available as a helper function
|
105
|
+
template = SandboxedErb::Template.new([MixinTest])
|
106
|
+
template.compile(str_template)
|
107
|
+
#pass the FauxController object as a context object called @controller (the keys are converted to member variables)
|
108
|
+
result = template.run({:controller=>faux_controller}, {})
|
109
|
+
|
110
|
+
#result = "mixin = TEST ABC"
|
111
|
+
|
112
|
+
|
113
|
+
== Contributing to sandboxed_erb
|
114
|
+
|
115
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
116
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
117
|
+
* Fork the project
|
118
|
+
* Start a feature/bugfix branch
|
119
|
+
* Commit and push until you are happy with your contribution
|
120
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
121
|
+
* 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.
|
122
|
+
|
123
|
+
== Copyright
|
124
|
+
|
125
|
+
Copyright (c) 2011 Mark Pentand. See LICENSE.txt for
|
126
|
+
further details.
|
127
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "sandboxed_erb"
|
18
|
+
gem.homepage = "http://github.com/markpent/SandboxedERB"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Run an erb template in a sandbox.}
|
21
|
+
gem.description = %Q{All your customers to extend your web application by exposing erb templates that can be safely run on your server within a sandbox.}
|
22
|
+
gem.email = "mark.pent@gmail.com"
|
23
|
+
gem.authors = ["MarkPent"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
Rake::TestTask.new(:test) do |test|
|
30
|
+
test.libs << 'lib' << 'test'
|
31
|
+
test.pattern = 'test/**/test_*.rb'
|
32
|
+
test.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
require 'rcov/rcovtask'
|
37
|
+
Rcov::RcovTask.new do |test|
|
38
|
+
test.libs << 'test'
|
39
|
+
test.pattern = 'test/**/test_*.rb'
|
40
|
+
test.verbose = true
|
41
|
+
test.rcov_opts << '--exclude "gems/*"'
|
42
|
+
end
|
43
|
+
|
44
|
+
task :default => :test
|
45
|
+
|
46
|
+
require 'rake/rdoctask'
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
48
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = "sandboxed_erb #{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/example/example.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem 'faker'
|
4
|
+
gem 'partialruby'
|
5
|
+
gem 'ruby_parser'
|
6
|
+
|
7
|
+
require 'date'
|
8
|
+
require 'faker'
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
11
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
12
|
+
require 'sandboxed_erb'
|
13
|
+
|
14
|
+
|
15
|
+
require 'controller.rb'
|
16
|
+
require 'note.rb'
|
17
|
+
require 'users.rb'
|
18
|
+
|
19
|
+
|
20
|
+
#an example helper to make functions available to the template...
|
21
|
+
module ExampleHelper
|
22
|
+
def link_to(title, href)
|
23
|
+
#silly example, but it shows how the mixins have access to the @controller context as an instance variable
|
24
|
+
if href.index(":").nil?
|
25
|
+
"<a href=\"#{@controller.base_url}/#{href}\">#{title}</a>"
|
26
|
+
else
|
27
|
+
"<a href=\"#{href}\">#{title}</a>"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_date(date, format)
|
32
|
+
if format == :short_date
|
33
|
+
date.strftime("%d %b %Y %H:%M")
|
34
|
+
else
|
35
|
+
"unknown format: #{format}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
#we will load both templates up and output them...
|
42
|
+
|
43
|
+
listing_sbhtml = File.open('listing.sbhtml') { |f| f.read }
|
44
|
+
|
45
|
+
notes_sbhtml = File.open('view_notes.sbhtml') { |f| f.read}
|
46
|
+
|
47
|
+
|
48
|
+
controller = Controller.new
|
49
|
+
users = Users.users(20)
|
50
|
+
|
51
|
+
|
52
|
+
listing_template = SandboxedErb::Template.new([ExampleHelper])
|
53
|
+
|
54
|
+
if !listing_template.compile(listing_sbhtml)
|
55
|
+
puts "Listing: #{listing_template.get_error}"
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
notes_template = SandboxedErb::Template.new([ExampleHelper])
|
61
|
+
|
62
|
+
if !notes_template.compile(notes_sbhtml)
|
63
|
+
puts "Notes: #{notes_template.get_error}"
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
result = listing_template.run({:controller=>controller}, {:users=>users})
|
71
|
+
if result.nil?
|
72
|
+
puts "Listing: #{listing_template.get_error}"
|
73
|
+
exit
|
74
|
+
end
|
75
|
+
|
76
|
+
File.open("listing.html", "w") { |f| f.write(result)}
|
77
|
+
|
78
|
+
result = notes_template.run({:controller=>controller}, {:user=>users[0], :users=>users})
|
79
|
+
if result.nil?
|
80
|
+
puts "Notes: #{notes_template.get_error}"
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
|
84
|
+
File.open("notes.html", "w") { |f| f.write(result)}
|
85
|
+
|
86
|
+
|
87
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<h1>User Notes</h1>
|
2
|
+
|
3
|
+
<table>
|
4
|
+
<tr>
|
5
|
+
<th>Name</th>
|
6
|
+
<th>Email</th>
|
7
|
+
<th>Phone</th>
|
8
|
+
<th></th>
|
9
|
+
</tr>
|
10
|
+
<% for user in users %>
|
11
|
+
<tr>
|
12
|
+
<td>
|
13
|
+
<%=link_to "#{user.first_name} #{user.last_name}", user.url_for(:edit)%>
|
14
|
+
</td>
|
15
|
+
<td>
|
16
|
+
<%=link_to user.email, "mailto: #{user.email}"%>
|
17
|
+
</td>
|
18
|
+
<td>
|
19
|
+
<%= user.phone%>
|
20
|
+
</td>
|
21
|
+
<td>
|
22
|
+
<%=link_to "Send Message", user.url_for(:send_message)%>
|
23
|
+
</td>
|
24
|
+
</tr>
|
25
|
+
<%end%>
|
26
|
+
</table>
|
data/example/note.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Note
|
2
|
+
|
3
|
+
attr_accessor :from
|
4
|
+
attr_accessor :subject
|
5
|
+
attr_accessor :message
|
6
|
+
attr_accessor :at
|
7
|
+
|
8
|
+
|
9
|
+
sandboxed_methods :from, :subject, :message, :at
|
10
|
+
|
11
|
+
def initialize(all_users)
|
12
|
+
@from = all_users[(rand * all_users.length).floor]
|
13
|
+
@subject = Faker::Lorem.sentence
|
14
|
+
@message = Faker::Lorem.paragraph
|
15
|
+
@at = DateTime.now - (rand * 10000)
|
16
|
+
end
|
17
|
+
end
|
data/example/users.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
class Users
|
2
|
+
|
3
|
+
def self.users(count)
|
4
|
+
res = []
|
5
|
+
for i in 0...count
|
6
|
+
res << User.new(i)
|
7
|
+
end
|
8
|
+
res
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class User
|
13
|
+
|
14
|
+
attr_accessor :id
|
15
|
+
attr_accessor :first_name
|
16
|
+
attr_accessor :last_name
|
17
|
+
attr_accessor :email
|
18
|
+
attr_accessor :phone
|
19
|
+
|
20
|
+
|
21
|
+
sandboxed_methods :id, :first_name, :last_name, :email, :phone, :notes, :url_for
|
22
|
+
|
23
|
+
def initialize(id)
|
24
|
+
@id = id
|
25
|
+
@first_name = Faker::Name.first_name
|
26
|
+
@last_name = Faker::Name.last_name
|
27
|
+
@email = Faker::Internet.email(@first_name)
|
28
|
+
@phone = Faker::PhoneNumber.phone_number
|
29
|
+
end
|
30
|
+
|
31
|
+
def set_sandbox_context(context)
|
32
|
+
@sandbox_context = context
|
33
|
+
end
|
34
|
+
|
35
|
+
def notes
|
36
|
+
@notes ||= build_notes
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_notes
|
40
|
+
res = []
|
41
|
+
for i in 0..(rand * 5 + 1).to_i
|
42
|
+
res << Note.new(@sandbox_context[:locals][:users])
|
43
|
+
end
|
44
|
+
res
|
45
|
+
end
|
46
|
+
|
47
|
+
def url_for(action)
|
48
|
+
if action == :edit
|
49
|
+
@sandbox_context[:controller].url_for(:controller=>:users, :action=>:edit, :id=>@id)
|
50
|
+
elsif action == :send_message
|
51
|
+
@sandbox_context[:controller].url_for(:controller=>:users, :action=>:send_message, :id=>@id)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<h1>Viewing Notes For <%=user.first_name%> <%=user.last_name%></h1>
|
2
|
+
|
3
|
+
<% for note in user.notes %>
|
4
|
+
<div>
|
5
|
+
<h2><%=note.subject%></h2>
|
6
|
+
<h3>From <%=note.from.first_name%> <%=note.from.last_name%> at <%=format_date(note.at, :short_date)%></h3>
|
7
|
+
<p><%=note.message%>
|
8
|
+
</div>
|
9
|
+
<%end%>
|