sandboxed_erb 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
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
@@ -0,0 +1,11 @@
1
+ class Controller
2
+
3
+
4
+ def url_for(options)
5
+ "/#{options[:controller]}/#{options[:action]}/#{options[:id]}"
6
+ end
7
+
8
+ def base_url
9
+ "http://some.url"
10
+ end
11
+ end
@@ -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%>