Safe Ruby Environment

M

Michael Neumann

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
 
G

gabriele renzi

il Wed, 21 Jul 2004 06:03:50 +0900, Michael Neumann
<[email protected]> ha scritto::

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

Michael Neumann

gabriele said:
il Wed, 21 Jul 2004 06:03:50 +0900, Michael Neumann
<[email protected]> 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 :)

Regards,

Michael
 
F

Florian Gross

Michael said:
Not that I need this feature, but I'd like it :)

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
 
T

ts

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

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


Guy Decoux
 
F

Florian Gross

ts said:
F> $SAFE = 5
F> eval(code, sandbox.send:)binding))
F> end
F> value = thread.value
F> 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
 
T

ts

F> I believe this one to be safe, but I'd prefer to be proven the opposite
F> 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
 
F

Florian Gross

ts said:
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
 
T

ts

F> Hm, which means that I have to call secure_object recursively on all
F> objects which the object itself references. (instance_variables,
F> 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%

F> Would this be enough or am I still overseeing something?

perhaps, I don't know


Guy Decoux
 
F

Florian Gross

Florian said:
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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,774
Messages
2,569,596
Members
45,144
Latest member
KetoBaseReviews
Top