safe eval?

A

Ara.T.Howard

i have a project i'm working on where i'd like to support complex
boolean/relational requests, where those requests must be satisfied on the
context of defined objects... i'm loath to create an entire parser/scanner
just to evaluate these expression when ruby's own is already written but also
don't want to risk using eval for the obvious reason. so, for example, i'll
have a command line option for a request:

prog.rb --request='a < 42 and b == true'


i can think of three approaches for evaluating such requests:

1) eval

request = 'a < 42 and b == true'
a = 42
b = true
eval request

2) code generation using ruby to evaluate (this protects against evil evals)

request = 'a < 42 and b == true'
a = 42
b = true

code = <<-code
a = #{ a }
b = #{ b }
p(#{ request })
code

res = `ruby -e '#{ code }'`

case res
when /true/o
when /false/o
else
end

3) full blown racc parser with associated context/evaluation logic...


* eval is attractive because i'd be done today, but it'd be too easy for someone
to do

prog.rb --request='a < 42 and b == true; raise "ha ha"'

* code generation is attractive for the same reason but feels hackish and slow


* the full blown racc parser just seems like alot of work to accomplish such a
small thing... then again perhaps it wouldn't be that bad...

can someone think of alternatives or variations that are simple and safe?


-a
--
===============================================================================
| EMAIL :: Ara [dot] T [dot] Howard [at] noaa [dot] gov
| PHONE :: 303.497.6469
| ADDRESS :: E/GC2 325 Broadway, Boulder, CO 80305-3328
| URL :: http://www.ngdc.noaa.gov/stp/
| TRY :: for l in ruby perl;do $l -e "print \"\x3a\x2d\x29\x0a\"";done
===============================================================================
 
T

ts

A> 1) eval

[...]

A> 2) code generation using ruby to evaluate (this protects against evil
A> evals)

If you really think that you are protected against the evil eval then just
try

A> request = 'a < 42 and b == true'

request = '(a < 42 && b == true; %x{ls})'

then replace ls with what you want ...

Use $SAFE = 4 in a new thread


Guy Decoux
 
A

Ara.T.Howard

A> 1) eval

[...]

A> 2) code generation using ruby to evaluate (this protects against evil
A> evals)

If you really think that you are protected against the evil eval then just
try

A> request = 'a < 42 and b == true'

request = '(a < 42 && b == true; %x{ls})'

then replace ls with what you want ...

Use $SAFE = 4 in a new thread

and this does protect against modifying globals.... hmmm. simple (could be
done today) and safe... sounds better than writing a parser/scanner...


-a
--
===============================================================================
| EMAIL :: Ara [dot] T [dot] Howard [at] noaa [dot] gov
| PHONE :: 303.497.6469
| ADDRESS :: E/GC2 325 Broadway, Boulder, CO 80305-3328
| URL :: http://www.ngdc.noaa.gov/stp/
| TRY :: for l in ruby perl;do $l -e "print \"\x3a\x2d\x29\x0a\"";done
===============================================================================
 
A

Ara.T.Howard

Just use this:


that looks sweet! i'll try it out later today... thanks!

-a

module Safe; end
class << Safe
# 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 occured.
#
# 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 {
$-w = nil

sandbox ||= Object.new.taint

yield(sandbox) if block_given?

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

return result, error
end
end

def safe(*args, &block)
Safe::safe(*args, &block)
end


Regards,
Florian Gross

--
===============================================================================
| EMAIL :: Ara [dot] T [dot] Howard [at] noaa [dot] gov
| PHONE :: 303.497.6469
| ADDRESS :: E/GC2 325 Broadway, Boulder, CO 80305-3328
| URL :: http://www.ngdc.noaa.gov/stp/
| TRY :: for l in ruby perl;do $l -e "print \"\x3a\x2d\x29\x0a\"";done
===============================================================================
 
T

ts

F> Just use this:

svg% cat b.rb
#!/usr/bin/ruby
module Safe; end
class << Safe
def safe(code, sandbox=nil)
error = nil

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

sandbox ||= Object.new.taint

yield(sandbox) if block_given?

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

return result, error
end
end

def safe(*args, &block)
Safe::safe(*args, &block)
end

safe('
class << a = []
def _dump(a)
$stderr.puts "More you make it complex, more it will be easy to break"
end
end
a')
svg%


Guy Decoux
 
F

Florian Gross

ts said:
safe('
class << a = []
def _dump(a)
$stderr.puts "More you make it complex, more it will be easy to break"
end
end
a')

Heh, good one. Thanks for pointing this out.

Try this fixed version:

module Safe
extend self

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

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

$SAFE = 5
$-w = nil

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
end

def safe(*args, &block)
Safe.safe(*args, &block)
end

Regards,
Florian Gross
 
T

ts

F> Try this fixed version:

I really don't know if I must post this :-(


svg% ls b.rb x.rb
ls: x.rb: No such file or directory
b.rb*
svg%

svg% cat b.rb
#!/usr/bin/ruby
module Safe
extend self

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)
immediate_classes = [Fixnum, Symbol, NilClass, TrueClass,
FalseClass]
return obj if immediate_classes.any? { |klass| klass === obj }
safe_dup = Object.instance_method:)dup).bind(obj)
safe_dup.call
end
end

def safe(*args, &block)
Safe.safe(*args, &block)
end

b = safe('
class << s = "`mv b.rb x.rb`"
def call
end
end
a = Object.new
ObjectSpace.define_finalizer(a, s)
a
')
svg%

svg% b.rb
svg%

svg% ls b.rb x.rb
ls: b.rb: No such file or directory
x.rb*
svg%




Guy Decoux
 
A

Ara.T.Howard

F> Try this fixed version:

I really don't know if I must post this :-(

yes, you must!

what do you do with your spare time guy!? watch out or a man in black wearing
dark glasses is going to knock on your door... ;-)

all this makes me start to think that i really must use a racc parser....
uggh

-a
svg% ls b.rb x.rb
ls: x.rb: No such file or directory
b.rb*
svg%

svg% cat b.rb
#!/usr/bin/ruby
module Safe
extend self

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)
immediate_classes = [Fixnum, Symbol, NilClass, TrueClass,
FalseClass]
return obj if immediate_classes.any? { |klass| klass === obj }
safe_dup = Object.instance_method:)dup).bind(obj)
safe_dup.call
end
end

def safe(*args, &block)
Safe.safe(*args, &block)
end

b = safe('
class << s = "`mv b.rb x.rb`"
def call
end
end
a = Object.new
ObjectSpace.define_finalizer(a, s)
a
')
svg%

svg% b.rb
svg%

svg% ls b.rb x.rb
ls: b.rb: No such file or directory
x.rb*
svg%




Guy Decoux

--
===============================================================================
| EMAIL :: Ara [dot] T [dot] Howard [at] noaa [dot] gov
| PHONE :: 303.497.6469
| ADDRESS :: E/GC2 325 Broadway, Boulder, CO 80305-3328
| URL :: http://www.ngdc.noaa.gov/stp/
| TRY :: for l in ruby perl;do $l -e "print \"\x3a\x2d\x29\x0a\"";done
===============================================================================
 
H

Hal Fulton

Ara.T.Howard said:
what do you do with your spare time guy!? watch out or a man in black wearing
dark glasses is going to knock on your door... ;-)

This comment is twice as scary coming from a .gov email address. :)

Guy: Don't take this the wrong way... I was once chatting with a couple
of other rubyists, and we were talking about how impressive your
knowledge was.

Then we started talking about how no one we had ever talked to had ever
seen you in person. So I submitted the tentative theory that you are
actually not one person at all; you are either a committee of several
computer science experts, or a network of Cray supercomputers.


Cheers,

Hal
 
A

Ara.T.Howard

This comment is twice as scary coming from a .gov email address. :)

and the suit is just back from the dry cleaners... now where are my
ray-bans...
Guy: Don't take this the wrong way... I was once chatting with a couple of
other rubyists, and we were talking about how impressive your knowledge was.

ditto. i tell ruby nubies here in house to include 'ts' when googling for
answers from c.l.r if they want to find the 'right' answer!
Then we started talking about how no one we had ever talked to had ever seen
you in person. So I submitted the tentative theory that you are actually not
one person at all; you are either a committee of several computer science
experts, or a network of Cray supercomputers.

or, most likely, a committee of computer science experts running a network of
Cray supercomputers

-a
--
===============================================================================
| EMAIL :: Ara [dot] T [dot] Howard [at] noaa [dot] gov
| PHONE :: 303.497.6469
| ADDRESS :: E/GC2 325 Broadway, Boulder, CO 80305-3328
| URL :: http://www.ngdc.noaa.gov/stp/
| TRY :: for l in ruby perl;do $l -e "print \"\x3a\x2d\x29\x0a\"";done
===============================================================================
 
F

Florian Gross

ts said:
b = safe('
class << s = "`mv b.rb x.rb`"
def call
end
end
a = Object.new
ObjectSpace.define_finalizer(a, s)
a
')

Heh, I don't actually regard this one as a bug of safe(), but more as
one of Ruby. I'm uncertain if matz agrees, however.

Personally, I have a more complete version of it that adds $SAFE-checks
to a lot of Ruby's built-in methods. (All methods of GC,
ObjectSpace.(define|add)_finalizer, Thread.new / .fork / .start /
..critical=, set_trace_func)

I'm pretty sure that there are more cases like this where $SAFE isn't
checked correctly in Ruby. If anybody wants to point out more of them, I
can try to come up with a way to secure them, but I'm unsure if this is
the best solution and if it will work all the time.

Actually, that's the reason of using a $SAFE-level of 5 and not 4 as one
would probably expect. :)

Here is the way I secure define_finalizer:

ObjectSpace.module_eval do
class << self
old_finalizer = instance_method:)define_finalizer)
define_method:)_define_finalizer) do |block, *args|
raise(SecurityError, "Penalizing finalizing") if $SAFE > 1
old_finalizer.bind(self).call(*args, &block)
end
def define_finalizer(*args, &block)
_define_finalizer(block, *args)
end

alias :add_finalizer :define_finalizer
end
end

If anybody wants to have the complete version with all the other added
checks, just let me know. I'll do some cleaning up and release the whole
thing in that case.

Regards,
Florian Gross
 
T

ts

F> Heh, I don't actually regard this one as a bug of safe(), but more as
F> one of Ruby. I'm uncertain if matz agrees, however.

Well, it's not really important for me : I was just able to run code not
expected.

F> Actually, that's the reason of using a $SAFE-level of 5 and not 4 as one
F> would probably expect. :)

plruby run with $SAFE = 12 :)


Guy Decoux
 

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,764
Messages
2,569,564
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top