Safe Ruby Environment

Hi,

Okay, there are the different $SAFE levels. But why not simply removing dangerous methods, like:

   undef `
   undef system
   undef require
   ...

or replacing them by your own?

I guess, this is as secure as any $SAFE level (of course it depends on which methods you are removing). Or am I missing something? The problem is that this way you can't run other "good" code next to your "bad" code (as it is possible with $SAFE).

It would be very nice to execute some Ruby code in such a reduced environment without affecting the other "good" code:

   env = Environment.new
   env.remove_method :system
   env.remove_constant :ENV
   env.remove_global "$0"
   ...
   env.eval dangerous_code

   # or
   env = Environment.fresh
   env.add_method :system
   env.add_constant :ENV, ENV
   ...

BTW, is this possible to implement in Ruby or a C extension? I guess not. Or would it work with two (or multiple) anonymous modules, one for the good code, one for the bad code, and then by removing all methods/constants/global variables outside those two modules?

Regards,

   Michael

I'm not sure, but maybe you simply want a thread running at $SAFE>4
and another one accessing the clean data?

···

il Wed, 21 Jul 2004 06:03:50 +0900, Michael Neumann <mneumann@ntecs.de> ha scritto::

gabriele renzi wrote:

···

il Wed, 21 Jul 2004 06:03:50 +0900, Michael Neumann > <mneumann@ntecs.de> ha scritto::

I'm not sure, but maybe you simply want a thread running at $SAFE>4
and another one accessing the clean data?

Not that I need this feature, but I'd like it :slight_smile:

Regards,

   Michael

Michael Neumann wrote:

gabriele renzi wrote:

I'm not sure, but maybe you simply want a thread running at $SAFE>4
and another one accessing the clean data?

Not that I need this feature, but I'd like it :slight_smile:

Here's an implementation:

   # Runs passed code in a relatively safe sandboxed environment.

···

#
   # You can pass a block which is called with the sandbox as its first
   # argument to apply custom changes to the sandbox environment.
   #
   # Returns an Array with the result of the executed code and
   # an exception, if one occurred.
   #
   # Example of usage:
   #
   # result, error = safe "1.0 / rand(10)"
   # puts if error then
   # "Error: #{error.inspect}"
   # else
   # result.inspect
   # end

   def safe(code, sandbox=nil)
     error = nil

     begin
       thread = Thread.new do
         $-w = nil

         sandbox ||= Object.new.taint

         yield(sandbox) if block_given?

         $SAFE = 5
         eval(code, sandbox.send(:binding))
       end
       value = thread.value
       result = Marshal.load(Marshal.dump(thread.value))
     rescue Exception => error
       error = Marshal.load(Marshal.dump(error))
     end

     return result, error
   end

However in current Ruby versions there is a way to escape the sandbox via ObjectSpace#define_finalizer. I suppose that this could be worked around by overloading it with a version that gets the $SAFE level of the caller via Binding.of_caller and then applies it to the passed-in handler via eval "$SAFE = #{caller_safe}", block.

But I'm not sure whether that solution would be 100% secure and I'd like to avoid adding a dependency to Binding.of_caller here.

Matz is aware of that problem (ts has discovered and reported it) and as far as I know it will be fixed in Ruby 1.8.2.

Regards,
  Michael

More regards,
Florian Gross

         $SAFE = 5
         eval(code, sandbox.send(:binding))
       end
       value = thread.value
       result = Marshal.load(Marshal.dump(thread.value))

it always an error to evaluate the result with a different value of $SAFE

Guy Decoux

ts wrote:

"F" == Florian Gross <flgr@ccan.de> writes:

> $SAFE = 5
> eval(code, sandbox.send(:binding))
> end
> value = thread.value
> result = Marshal.load(Marshal.dump(thread.value))
it always an error to evaluate the result with a different value of $SAFE

Hm, right. It was an old version, sorry for that. This one should work correctly: (Except the ObjectSpace#define_finalizer problem)

  # Runs passed code in a relatively safe sandboxed environment.
  # # You can pass a block which is called with the sandbox as its first # argument to apply custom changes to the sandbox environment.
  # # Returns an Array with the result of the executed code and
  # an exception, if one occurred.
  # # Example of usage:
  # # result, error = safe "1.0 / rand(10)"
  # puts if error then
  # "Error: #{error.inspect}"
  # else
  # result.inspect
  # end
  def safe(code, sandbox = nil)
    error, result = nil, nil

    begin
      thread = Thread.new do
        sandbox ||= Object.new.taint
        yield(sandbox) if block_given?

        $-w = nil
        $SAFE = 5

        eval(code, sandbox.send(:binding))
      end
      result = secure_object(thread.value)
    rescue Exception => error
      error = secure_object(error)
    end

    return result, error
  end

  def secure_object(obj)
    # We can't dup immediate values. But that's no problem
    # because most of them can't have any singleton methods
    # anyway. (nil, true and false can, but they can't be
    # defined in safe contexts.)
    immediate_classes = [Fixnum, Symbol, NilClass, TrueClass, FalseClass]
    return obj if immediate_classes.any? { |klass| klass === obj }

    # Dup won't copy any singleton methods and without any
    # of them the Object will be safe. (But we can't call
    # the Object's .dup because it might be evil already.)
    safe_dup = Object.instance_method(:dup).bind(obj)
    safe_dup.call
  end

I believe this one to be safe, but I'd prefer to be proven the opposite by you instead of some malicious attacker.

Regards,
Florian Gross

I believe this one to be safe, but I'd prefer to be proven the opposite
by you instead of some malicious attacker.

it depend how you use the object after this ...

svg% cat b.rb
#!/usr/bin/ruby
def safe(code, sandbox = nil)
  error, result = nil, nil
  begin
    thread = Thread.new do
      sandbox ||= Object.new.taint
      yield(sandbox) if block_given?
      $-w = nil
      $SAFE = 5
      eval(code, sandbox.send(:binding))
    end
    result = secure_object(thread.value)
  rescue Exception => error
    error = secure_object(error)
  end
  return result, error
end

def secure_object(obj)
  # We can't dup immediate values. But that's no problem
  # because most of them can't have any singleton methods
  # anyway. (nil, true and false can, but they can't be
  # defined in safe contexts.)
  immediate_classes = [Fixnum, Symbol, NilClass, TrueClass, FalseClass]
  return obj if immediate_classes.any? { |klass| klass === obj }
  # Dup won't copy any singleton methods and without any
  # of them the Object will be safe. (But we can't call
  # the Object's .dup because it might be evil already.)
  safe_dup = Object.instance_method(:dup).bind(obj)
  safe_dup.call
end

p safe(IO::read("aa"))
svg%

svg% b.rb
[#<Object:0x40098e18 @a=hello :-)>, nil]
svg%

svg% cat b.rb
cat: b.rb: No such file or directory
svg%

Guy Decoux

ts wrote:

it depend how you use the object after this ...

Hm, which means that I have to call secure_object recursively on all objects which the object itself references. (instance_variables, contents of Arrays)

Would this be enough or am I still overseeing something?

Regards,
Florian Gross

Hm, which means that I have to call secure_object recursively on all
objects which the object itself references. (instance_variables,
contents of Arrays)

You have found, aa was

svg% cat aa
a = Object.new
b = "sss"
class << b
   def inspect
      system("rm b.rb")
      "hello :-)"
   end
end
a.instance_variable_set("@a", b)
a
svg%

Would this be enough or am I still overseeing something?

perhaps, I don't know

Guy Decoux

Florian Gross wrote:

Hm, which means that I have to call secure_object recursively on all objects which the object itself references. (instance_variables, contents of Arrays)

And secure_object needs to raise an Exception when
secure_tainted.bind(secure_class.bind(obj).call).call (Object is an instance of an insecure class).

Currently this also works:

safe "Class.new { def inspect; puts 'foo'; end }.new"

Regards,
Florian Gross