├── .gitignore ├── .rspec ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── constant_whitelist.rb ├── make_safe_code.rb ├── method_whitelist.rb ├── safe_ruby.rb └── safe_ruby │ ├── runner.rb │ └── version.rb ├── safe_ruby.gemspec └── spec ├── safe_ruby_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .DS_STORE 3 | lib/.DS_STORE -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | safe_ruby (1.0.1) 5 | childprocess (>= 0.3.9) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | childprocess (0.7.1) 11 | ffi (~> 1.0, >= 1.0.11) 12 | coderay (1.1.2) 13 | diff-lcs (1.3) 14 | ffi (1.9.18) 15 | method_source (0.8.2) 16 | pry (0.10.4) 17 | coderay (~> 1.1.0) 18 | method_source (~> 0.8.1) 19 | slop (~> 3.4) 20 | rake (12.1.0) 21 | rspec (3.6.0) 22 | rspec-core (~> 3.6.0) 23 | rspec-expectations (~> 3.6.0) 24 | rspec-mocks (~> 3.6.0) 25 | rspec-core (3.6.0) 26 | rspec-support (~> 3.6.0) 27 | rspec-expectations (3.6.0) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.6.0) 30 | rspec-mocks (3.6.0) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.6.0) 33 | rspec-support (3.6.0) 34 | slop (3.6.0) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | pry 41 | rake 42 | rspec 43 | safe_ruby! 44 | 45 | BUNDLED WITH 46 | 1.15.4 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 heruku 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Safe Ruby 2 | ========= 3 | 4 | Safe Ruby provides a way to run untrusted ruby code outside of the current process in a safe environment. 5 | Creating this environment is largery based on jruby sandbox, whitelisting the methods one can use on potentially 6 | dangerous classes. Constants are also whitelisted, eliminating some core ruby functionality such as spawning 7 | another process. 8 | 9 | Getting Started 10 | ============== 11 | 12 | Run `gem install safe_ruby` in your terminal, then `require 'safe_ruby'` in your app and you're ready to go. 13 | 14 | Examples 15 | ======== 16 | 17 | Evaluating ruby code 18 | 19 | ```ruby 20 | SafeRuby.eval('[1,2,3].map{ |n| n + 1 }') #=> [2, 3, 4] 21 | 22 | SafeRuby.eval('system("rm *")') #=> system is unavailable 23 | 24 | SafeRuby.eval('Kernel.abort') #=> undefined method `abort' for Kernel:Module 25 | ``` 26 | 27 | Default timeout for evaluating is 5 seconds, but this can be specified. 28 | 29 | ```ruby 30 | SafeRuby.eval('loop{}') # 31 | 32 | SafeRuby.eval('loop{}', timeout: 1) # 33 | ``` 34 | 35 | This library was built for a codeacademy-style tutoring app, so checking answers is built into the SafeRuby class 36 | 37 | ```ruby 38 | SafeRuby.check('[1,2,3].map{ |n| n + 1 }', '[2,3,4]') #=> true 39 | ``` 40 | 41 | In this example, the second argument(expected answer) can also be untrusted, it will be run safely. 42 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new 7 | 8 | desc 'Run specs' 9 | task :default => :spec 10 | -------------------------------------------------------------------------------- /lib/constant_whitelist.rb: -------------------------------------------------------------------------------- 1 | ALLOWED_CONSTANTS= [ 2 | :Object, :Module, :Class, :BasicObject, :Kernel, :NilClass, :NIL, :Data, :TrueClass, :TRUE, :FalseClass, :FALSE, :Encoding, 3 | :Comparable, :Enumerable, :String, :Symbol, :Exception, :SystemExit, :SignalException, :Interrupt, :StandardError, :TypeError, 4 | :ArgumentError, :IndexError, :KeyError, :RangeError, :ScriptError, :SyntaxError, :LoadError, :NotImplementedError, :NameError, 5 | :NoMethodError, :RuntimeError, :SecurityError, :NoMemoryError, :EncodingError, :SystemCallError, :Errno, :ZeroDivisionError, 6 | :FloatDomainError, :Numeric, :Integer, :Fixnum, :Float, :Bignum, :Array, :Hash, :Struct, :RegexpError, :Regexp, 7 | :MatchData, :Marshal, :Range, :IOError, :EOFError, :IO, :STDIN, :STDOUT, :STDERR, :Time, :Random, 8 | :Signal, :Proc, :LocalJumpError, :SystemStackError, :Method, :UnboundMethod, :Binding, :Math, :Enumerator, 9 | :StopIteration, :RubyVM, :Thread, :TOPLEVEL_BINDING, :ThreadGroup, :Mutex, :ThreadError, :Fiber, :FiberError, :Rational, :Complex, 10 | :RUBY_VERSION, :RUBY_RELEASE_DATE, :RUBY_PLATFORM, :RUBY_PATCHLEVEL, :RUBY_REVISION, :RUBY_DESCRIPTION, :RUBY_COPYRIGHT, :RUBY_ENGINE, 11 | :TracePoint, :ARGV, :Gem, :RbConfig, :Config, :CROSS_COMPILING, :Date, :ConditionVariable, :Queue, :SizedQueue, :MonitorMixin, :Monitor, 12 | :Exception2MessageMapper, :IRB, :RubyToken, :RubyLex, :Readline, :RUBYGEMS_ACTIVATION_MONITOR 13 | ] 14 | -------------------------------------------------------------------------------- /lib/make_safe_code.rb: -------------------------------------------------------------------------------- 1 | MAKE_SAFE_CODE = <<-STRING 2 | def keep_singleton_methods(klass, singleton_methods) 3 | klass = Object.const_get(klass) 4 | singleton_methods = singleton_methods.map(&:to_sym) 5 | undef_methods = (klass.singleton_methods - singleton_methods) 6 | 7 | undef_methods.each do |method| 8 | klass.singleton_class.send(:undef_method, method) 9 | end 10 | 11 | end 12 | 13 | def keep_methods(klass, methods) 14 | klass = Object.const_get(klass) 15 | methods = methods.map(&:to_sym) 16 | undef_methods = (klass.methods(false) - methods) 17 | undef_methods.each do |method| 18 | klass.send(:undef_method, method) 19 | end 20 | end 21 | 22 | def clean_constants 23 | (Object.constants - #{ALLOWED_CONSTANTS}).each do |const| 24 | Object.send(:remove_const, const) if defined?(const) 25 | end 26 | end 27 | 28 | keep_singleton_methods(:Kernel, #{KERNEL_S_METHODS}) 29 | keep_singleton_methods(:Symbol, #{SYMBOL_S_METHODS}) 30 | keep_singleton_methods(:String, #{STRING_S_METHODS}) 31 | keep_singleton_methods(:IO, #{IO_S_METHODS}) 32 | 33 | keep_methods(:Kernel, #{KERNEL_METHODS}) 34 | keep_methods(:NilClass, #{NILCLASS_METHODS}) 35 | keep_methods(:TrueClass, #{TRUECLASS_METHODS}) 36 | keep_methods(:FalseClass, #{FALSECLASS_METHODS}) 37 | keep_methods(:Enumerable, #{ENUMERABLE_METHODS}) 38 | keep_methods(:String, #{STRING_METHODS}) 39 | Kernel.class_eval do 40 | def `(*args) 41 | raise NoMethodError, "` is unavailable" 42 | end 43 | 44 | def system(*args) 45 | raise NoMethodError, "system is unavailable" 46 | end 47 | end 48 | 49 | clean_constants 50 | 51 | STRING 52 | -------------------------------------------------------------------------------- /lib/method_whitelist.rb: -------------------------------------------------------------------------------- 1 | IO_S_METHODS = %w[ 2 | new 3 | foreach 4 | open 5 | ] 6 | 7 | KERNEL_S_METHODS = %w[ 8 | Array 9 | binding 10 | block_given? 11 | catch 12 | chomp 13 | chomp! 14 | chop 15 | chop! 16 | eval 17 | fail 18 | Float 19 | format 20 | global_variables 21 | gsub 22 | gsub! 23 | Integer 24 | iterator? 25 | lambda 26 | local_variables 27 | loop 28 | method_missing 29 | proc 30 | raise 31 | scan 32 | split 33 | sprintf 34 | String 35 | sub 36 | sub! 37 | throw 38 | ].freeze 39 | 40 | SYMBOL_S_METHODS = %w[ 41 | all_symbols 42 | ].freeze 43 | 44 | STRING_S_METHODS = %w[ 45 | new 46 | ].freeze 47 | 48 | KERNEL_METHODS = %w[ 49 | == 50 | 51 | ray 52 | nding 53 | ock_given? 54 | tch 55 | omp 56 | omp! 57 | op 58 | op! 59 | ass 60 | clone 61 | dup 62 | eql? 63 | equal? 64 | eval 65 | fail 66 | Float 67 | format 68 | freeze 69 | frozen? 70 | global_variables 71 | gsub 72 | gsub! 73 | hash 74 | id 75 | initialize_copy 76 | inspect 77 | instance_eval 78 | instance_of? 79 | instance_variables 80 | instance_variable_get 81 | instance_variable_set 82 | instance_variable_defined? 83 | Integer 84 | is_a? 85 | iterator? 86 | kind_of? 87 | lambda 88 | local_variables 89 | loop 90 | methods 91 | method_missing 92 | nil? 93 | private_methods 94 | print 95 | proc 96 | protected_methods 97 | public_methods 98 | raise 99 | remove_instance_variable 100 | respond_to? 101 | respond_to_missing? 102 | scan 103 | send 104 | singleton_methods 105 | singleton_method_added 106 | singleton_method_removed 107 | singleton_method_undefined 108 | split 109 | sprintf 110 | String 111 | sub 112 | sub! 113 | taint 114 | tainted? 115 | throw 116 | to_a 117 | to_s 118 | type 119 | untaint 120 | __send__ 121 | ].freeze 122 | 123 | NILCLASS_METHODS = %w[ 124 | & 125 | inspect 126 | nil? 127 | to_a 128 | to_f 129 | to_i 130 | to_s 131 | ^ 132 | | 133 | ].freeze 134 | 135 | SYMBOL_METHODS = %w[ 136 | === 137 | id2name 138 | inspect 139 | to_i 140 | to_int 141 | to_s 142 | to_sym 143 | ].freeze 144 | 145 | TRUECLASS_METHODS = %w[ 146 | & 147 | to_s 148 | ^ 149 | | 150 | ].freeze 151 | 152 | FALSECLASS_METHODS = %w[ 153 | & 154 | to_s 155 | ^ 156 | | 157 | ].freeze 158 | 159 | ENUMERABLE_METHODS = %w[ 160 | all? 161 | any? 162 | collect 163 | detect 164 | each_with_index 165 | entries 166 | find 167 | find_all 168 | grep 169 | include? 170 | inject 171 | map 172 | max 173 | member? 174 | min 175 | partition 176 | reject 177 | select 178 | sort 179 | sort_by 180 | to_a 181 | zip 182 | ].freeze 183 | 184 | STRING_METHODS = %w[ 185 | % 186 | * 187 | + 188 | << 189 | <=> 190 | == 191 | =~ 192 | capitalize 193 | capitalize! 194 | casecmp 195 | center 196 | chomp 197 | chomp! 198 | chop 199 | chop! 200 | concat 201 | count 202 | crypt 203 | delete 204 | delete! 205 | downcase 206 | downcase! 207 | dump 208 | each 209 | each_byte 210 | each_line 211 | empty? 212 | eql? 213 | gsub 214 | gsub! 215 | hash 216 | hex 217 | include? 218 | index 219 | initialize 220 | initialize_copy 221 | insert 222 | inspect 223 | intern 224 | length 225 | ljust 226 | lines 227 | lstrip 228 | lstrip! 229 | match 230 | next 231 | next! 232 | oct 233 | replace 234 | reverse 235 | reverse! 236 | rindex 237 | rjust 238 | rstrip 239 | rstrip! 240 | scan 241 | size 242 | slice 243 | slice! 244 | split 245 | squeeze 246 | squeeze! 247 | strip 248 | strip! 249 | start_with? 250 | sub 251 | sub! 252 | succ 253 | succ! 254 | sum 255 | swapcase 256 | swapcase! 257 | to_f 258 | to_i 259 | to_s 260 | to_str 261 | to_sym 262 | tr 263 | tr! 264 | tr_s 265 | tr_s! 266 | upcase 267 | upcase! 268 | upto 269 | [] 270 | []= 271 | ].freeze 272 | -------------------------------------------------------------------------------- /lib/safe_ruby.rb: -------------------------------------------------------------------------------- 1 | require 'childprocess' 2 | require_relative 'method_whitelist' 3 | require_relative 'constant_whitelist' 4 | require_relative 'make_safe_code' 5 | require_relative 'safe_ruby/runner' 6 | require_relative 'safe_ruby/version' 7 | 8 | class SafeRuby 9 | end 10 | -------------------------------------------------------------------------------- /lib/safe_ruby/runner.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | 3 | class EvalError < StandardError 4 | def initialize(msg); super; end 5 | end 6 | 7 | class SafeRuby 8 | DEFAULTS = { timeout: 5, 9 | raise_errors: true } 10 | 11 | def initialize(code, options={}) 12 | options = DEFAULTS.merge(options) 13 | 14 | @code = code 15 | @raise_errors = options[:raise_errors] 16 | @timeout = options[:timeout] 17 | end 18 | 19 | def self.eval(code, options={}) 20 | new(code, options).eval 21 | end 22 | 23 | def eval 24 | temp = build_tempfile 25 | read, write = IO.pipe 26 | ChildProcess.build("ruby", temp.path).tap do |process| 27 | process.io.stdout = write 28 | process.io.stderr = write 29 | process.start 30 | begin 31 | process.poll_for_exit(@timeout) 32 | rescue ChildProcess::TimeoutError => e 33 | process.stop # tries increasingly harsher methods to kill the process. 34 | return e.message 35 | end 36 | write.close 37 | temp.unlink 38 | end 39 | 40 | data = read.read 41 | begin 42 | Marshal.load(data) 43 | rescue => e 44 | if @raise_errors 45 | raise data 46 | else 47 | return data 48 | end 49 | end 50 | end 51 | 52 | def self.check(code, expected) 53 | eval(code) == eval(expected) 54 | end 55 | 56 | 57 | private 58 | 59 | def build_tempfile 60 | file = Tempfile.new('saferuby') 61 | file.write(MAKE_SAFE_CODE) 62 | file.write <<-STRING 63 | result = eval(%q(#{@code})) 64 | print Marshal.dump(result) 65 | STRING 66 | file.rewind 67 | file 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/safe_ruby/version.rb: -------------------------------------------------------------------------------- 1 | class SafeRuby 2 | MAJOR_VERSION = 1 3 | MINOR_VERSION = 0 4 | RELEASE_VERSION = 3 5 | 6 | VERSION = [MAJOR_VERSION, MINOR_VERSION, RELEASE_VERSION].join('.') 7 | end 8 | -------------------------------------------------------------------------------- /safe_ruby.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'safe_ruby/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'safe_ruby' 8 | s.version = SafeRuby::VERSION 9 | s.date = '2013-12-04' 10 | s.authors = ["Uku Taht"] 11 | s.email = 'uku.taht@gmail.com' 12 | 13 | s.summary = "Run untrusted ruby code in a safe environment" 14 | s.description = "Evaluates ruby code by writing it to a tempfile and spawning a child process. Uses a whitelist of methods and constants to keep, for example one cannot run system commands in the environment created by this gem. The environment created by the untrusted code does not leak out into the parent process." 15 | s.homepage = 'http://rubygems.org/gems/safe_ruby' 16 | s.license = 'MIT' 17 | 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.files = `git ls-files`.split("\n") 20 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 21 | s.require_paths = ["lib"] 22 | 23 | s.add_runtime_dependency 'childprocess', '>= 0.3.9' 24 | 25 | s.add_development_dependency 'pry' 26 | s.add_development_dependency 'rake' 27 | s.add_development_dependency 'rspec' 28 | end 29 | -------------------------------------------------------------------------------- /spec/safe_ruby_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SafeRuby do 4 | describe '#eval' do 5 | it 'allows basic operations' do 6 | expect(SafeRuby.eval('4 + 5')).to eq 9 7 | expect(SafeRuby.eval('[4, 5].map{|n| n+1}')).to eq [5 ,6] 8 | end 9 | 10 | it 'returns correct object' do 11 | expect(SafeRuby.eval('[1,2,3]')).to eq [1,2,3] 12 | end 13 | 14 | MALICIOUS_OPERATIONS = [ 15 | "system('rm *')", 16 | "`rm *`", 17 | "Kernel.abort", 18 | "cat spec/spec_helper.rb", 19 | "File.class_eval { `echo Hello` }", 20 | "FileUtils.class_eval { `echo Hello` }", 21 | "Dir.class_eval { `echo Hello` }", 22 | "FileTest.class_eval { `echo Hello` }", 23 | "File.eval \"`echo Hello`\"", 24 | "FileUtils.eval \"`echo Hello`\"", 25 | "Dir.eval \"`echo Hello`\"", 26 | "FileTest.eval \"`echo Hello`\"", 27 | "File.instance_eval { `echo Hello` }", 28 | "FileUtils.instance_eval { `echo Hello` }", 29 | "Dir.instance_eval { `echo Hello` }", 30 | "FileTest.instance_eval { `echo Hello` }", 31 | "f=IO.popen('uname'); f.readlines; f.close", 32 | "IO.binread('/etc/passwd')", 33 | "IO.read('/etc/passwd')", 34 | ] 35 | 36 | MALICIOUS_OPERATIONS.each do |op| 37 | it "protects from malicious operations like (#{op})" do 38 | expect{ 39 | SafeRuby.eval(op) 40 | }.to raise_error RuntimeError 41 | end 42 | end 43 | 44 | describe "options" do 45 | describe "timeout" do 46 | it 'defaults to a 5 second timeout' do 47 | time = Benchmark.realtime do 48 | SafeRuby.eval('(1..100000).map {|n| n**100}') 49 | end 50 | expect(time).to be_within(0.5).of(5) 51 | end 52 | 53 | it 'allows custom timeout' do 54 | time = Benchmark.realtime do 55 | SafeRuby.eval('(1..100000).map {|n| n**100}', timeout: 1) 56 | end 57 | expect(time).to be_within(0.5).of(1) 58 | end 59 | end 60 | 61 | describe "raising errors" do 62 | it "defaults to raising errors" do 63 | expect{ SafeRuby.eval("asdasdasd") }.to raise_error RuntimeError 64 | end 65 | 66 | it "allows not raising errors" do 67 | expect {SafeRuby.eval("asdasd", raise_errors: false)}.to_not raise_error 68 | end 69 | end 70 | end 71 | 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'safe_ruby' 2 | require 'benchmark' 3 | 4 | RSpec.configure do |config| 5 | config.run_all_when_everything_filtered = true 6 | config.filter_run :focus 7 | 8 | config.order = 'random' 9 | end 10 | --------------------------------------------------------------------------------