Not just $SAFE, but damn $SAFE

I've been toying with an IRC bot that takes input from users in channel,
evals it, and returns the result.

Doing so safely has proved to be a challenge -- the biggest problem
being that you can't trust any method on the returned object.

Here's our solution, and I'd love to know if anyone can break it.

module Safe
  def class
    super
  end
end

def safe_to_s(obj)
  t = Thread.new {
    $SAFE = 4
    obj.to_s
  }
  o = t.value
  class << o
    include Safe
  end
  if String == o.class
    o
  else
    raise SecurityError
  end
end

def safe_eval(code)
  t = Thread.new {
     $SAFE = 4
     eval(code)
  }
  t.value
end

puts(safe_to_s(safe_eval("exit! # or variations")))

puts "Made it!"

Consider this a challenge. The ways I broke simpler variants was to try
to trick safe_to_s into calling methods outside of the safe Thread.

Ari

Here's our solution, and I'd love to know if anyone can break it.

module Safe
  def class
    super
  end
end

def safe_to_s(obj)
  t = Thread.new {
    $SAFE = 4
    obj.to_s
  }
  o = t.value
  class << o
    include Safe
  end
  if String == o.class

better make it
       String === o

    o
  else
    raise SecurityError
  end
end

def safe_eval(code)
  t = Thread.new {
     $SAFE = 4
     eval(code)
  }
  t.value
end

puts(safe_to_s(safe_eval("exit! # or variations")))

puts "Made it!"

Take a look at [107071].

We had quite some fun in #ruby-lang nearly 1 year ago trying to break
Florian Groß' rubdo; the underlying code (safe.rb) was a quite more
involved, though.
It soon became apparent that it would be impossible to prevent DOS
attacks to rubdo:
* it was possible to block all threads with a slow builtin method (in C) like
  Bignum#** (and overcome the timeout mechanism)
* Thread.new and Object#define_finalizer proved to be evil (he had to
  disable them)
* symbols are not GCed; one could easily make flgr's machine swap to
  death by creating new symbols repeatedly

Most of them can be addressed with rlimit but he was using win32 :slight_smile:

···

On Thu, Sep 02, 2004 at 03:47:50PM +0900, Aredridel wrote:

--
Running Debian GNU/Linux Sid (unstable)
batsman dot geo at yahoo dot com

Aredridel wrote:

I've been toying with an IRC bot that takes input from users in channel,
evals it, and returns the result.

Doing so safely has proved to be a challenge -- the biggest problem
being that you can't trust any method on the returned object.

Here's our solution, and I'd love to know if anyone can break it.

It is easily breakable because of singleton methods. I've attached safe.rb which ought to be secure now (hello ts ;)) that ObjectSpace.define_finalizer doesn't allow one to escape the sandbox anymore.

However, as batsman already mentioned all this still doesn't help much against DoS attacks. (You have to protect against those on the OS level right now.)

Regards,
Florian Gross

safe.rb (2.14 KB)

I asked this question quite some time ago, doing the exact same thing (because having programmable bots that are also safe is really neat!).

Matz provided exactly what I wanted. Once you get your object back, you can't trust it, but you can make a new string from it with relative safety:

sprime = String.new( tainted_string )

This gets around most kinds of method hacking.

···

--
Dave Fayram
  kirindave@lensmen.net
  dfayram@gmail.com
--

Aredridel <aredridel@nbtsc.org> wrote:

I've been toying with an IRC bot that takes input from users in channel,
evals it, and returns the result.

Doing so safely has proved to be a challenge -- the biggest problem
being that you can't trust any method on the returned object.

Here's our solution, and I'd love to know if anyone can break it.

module Safe
  def class
    super
  end
end

def safe_to_s(obj)
  t = Thread.new {
    $SAFE = 4
    obj.to_s
  }
  o = t.value
  class << o
    include Safe
  end
  if String == o.class
    o
  else
    raise SecurityError
  end
end

def safe_eval(code)
  t = Thread.new {
     $SAFE = 4
     eval(code)
  }
  t.value
end

puts(safe_to_s(safe_eval("exit! # or variations")))

puts "Made it!"

Consider this a challenge. The ways I broke simpler variants was to try
to trick safe_to_s into calling methods outside of the safe Thread.

Ari

Most of them can be addressed with rlimit but he was
using win32 :slight_smile:

Yes well, this is what I was referring to when I said
Ruby needed its own limiting factors built-in to the
interpreter. Not everyone has a machine that is unix
based. There needs to be..
1) memory limiting
2) process limiting
3) file limiting and usage
4) etc. I am sure there are others

--David Ross

···

--- Mauricio Fernández <batsman.geo@yahoo.com> wrote:

--
Running Debian GNU/Linux Sid (unstable)
batsman dot geo at yahoo dot com

__________________________________
Do you Yahoo!?
Yahoo! Mail - 50x more storage than other providers!
http://promotions.yahoo.com/new_mail

Oh, and be careful about calling methods on the 'safe' return value. Object#inspect is recursive which means that it will be called on every element of Arrays, Structs etc. (and those aren't filtered through secure_object right now.)

It is easily breakable because of singleton methods. I've attached
safe.rb which ought to be secure now (hello ts ;)) that
ObjectSpace.define_finalizer doesn't allow one to escape the sandbox
anymore.

Sincerely I don't understand (I know, I'm stupid) but why don't you define
classes that you want to trust and give a result only when an object
belong to theses classes ?

Guy Decoux

What if I make Bad#to_str in this context?

Try this on:

class EvilStr < String
  def to_str
    puts "Haha2"
  end
end

class Bad
  def to_str
    puts "Haha!"
    EvilStr.new("Boo")
  end
end

puts String.new(Bad.new.taint)

···

On Fri, 2004-09-03 at 02:01 +0900, Dave Fayram wrote:

I asked this question quite some time ago, doing the exact same thing (because having programmable bots that are also safe is really neat!).

Matz provided exactly what I wanted. Once you get your object back, you can't trust it, but you can make a new string from it with relative safety:

sprime = String.new( tainted_string )

This gets around most kinds of method hacking.

ts wrote:

> It is easily breakable because of singleton methods. I've attached > safe.rb which ought to be secure now (hello ts ;)) that > ObjectSpace.define_finalizer doesn't allow one to escape the sandbox > anymore.

Sincerely I don't understand (I know, I'm stupid) but why don't you define
classes that you want to trust and give a result only when an object
belong to theses classes ?

An user might want to use the sandbox with custom classes. And I'm not sure if the check should be applied recursively or if an user is expected to be careful himself.

Regards,
Florian Gross

Sincerely I don't understand (I know, I'm stupid) but why don't you define
classes that you want to trust and give a result only when an object
belong to theses classes ?

The problem is in finding out what class something is.

obj.class # Bad -- class can be overridden to be evil

obj.instance_of? # Bad -- same as #class

Class === obj # Bad, because you can define an evil subclass.

Aredridel wrote:

Sincerely I don't understand (I know, I'm stupid) but why don't you define
classes that you want to trust and give a result only when an object
belong to theses classes ?

The problem is in finding out what class something is.

obj.class # Bad -- class can be overridden to be evil

obj.instance_of? # Bad -- same as #class

Class === obj # Bad, because you can define an evil subclass.

To find the class you can use :

class ClassFinder
   @@classCall = Object.instance_method(:class)
   def ClassFinder.class(obj)
     @@classCall.bind(obj).call
   end
end

class Bad
   def class
     Object
   end
end

puts Bad.new.class
puts ClassFinder.class(Bad.new)

outputs :
Object
Bad

Walt

···

--
Walter Szewelanczyk
IS Director
M.W. Sewall & CO. email : walter@mwsewall.com
259 Front St. Phone : (207) 442-7994 x 128
Bath, ME 04530 Fax : (207) 443-6284

An user might want to use the sandbox with custom classes. And I'm not
sure if the check should be applied recursively or if an user is
expected to be careful himself.

An user might want to write stupid thing, why your safe module must do
stupid thing ?

Guy Decoux

Class === obj # Bad, because you can define an evil subclass.

This is not a problem.

1) if you use `case', you can trust the result (i.e. an object can't "lie")

2) you know that some methods are safe. For example, if you have a String
    or an Exception use String.new(obj). If you have an Integer or a
    Bignum, use Integer(String.new(obj.to_s)). If you have a Time object
    use Time.at(obj), etc

    Now, if you have a container (Array, Hash, ...) just create a new
    container and store it only "safe" objects.

Guy Decoux

Walt, that's genius. That one's getting stowed somewhere for posterity.

Ari

···

On Fri, 2004-09-03 at 05:50 +0900, Walter Szewelanczyk wrote:

To find the class you can use :

class ClassFinder
   @@classCall = Object.instance_method(:class)
   def ClassFinder.class(obj)
     @@classCall.bind(obj).call
   end
end

Brilliant!

martin

···

Walter Szewelanczyk <walter@mwsewall.com> wrote:

To find the class you can use :

class ClassFinder
   @@classCall = Object.instance_method(:class)
   def ClassFinder.class(obj)
     @@classCall.bind(obj).call
   end
end

ts wrote:

> An user might want to use the sandbox with custom classes. And I'm not > sure if the check should be applied recursively or if an user is > expected to be careful himself.
An user might want to write stupid thing, why your safe module must do
stupid thing ?

Because sometimes the user wants to do a non-stupid, but still unexpected thing and then flexibility is good. But I think it would maybe be a good idea to allow white lists via an option to the safe()-call.

Regards,
Florian Gross

> Class === obj # Bad, because you can define an evil subclass.

This is not a problem.

1) if you use `case', you can trust the result (i.e. an object can't "lie")

Except that by using case, I can have an EvilString < String, for which
that returns true.

2) you know that some methods are safe. For example, if you have a String
    or an Exception use String.new(obj). If you have an Integer or a
    Bignum, use Integer(String.new(obj.to_s)). If you have a Time object
    use Time.at(obj), etc

    Now, if you have a container (Array, Hash, ...) just create a new
    container and store it only "safe" objects.

Yup. That's similar to what we're doing. The problem lies in to_s, in
that our eventual goal is to output text. Any deep data structure is
harder to deal with.

Ari

···

On Fri, 2004-09-03 at 17:08 +0900, ts wrote:

Because sometimes the user wants to do a non-stupid, but still
unexpected thing and then flexibility is good. But I think it would
maybe be a good idea to allow white lists via an option to the safe()-call.

and where is the problem ?

There are 2 cases :

  * some classes are safe, because you can safely use a method to create an
    object.

    For example Symbol, NilClass, TrueClass, FalseClass, Fixnum, Bignum,
    Float, String, Exception, Regexp, Time, Range, Array, Hash

    your module can handle these classes

  * for other classes, define a protocol

    - when you enter in #safe store in an array, all classes which are not
      tainted (test it with Kernel#tainted?) and which define the method
      ::secure_it

    - when the class of the result respond to this method, call it with the
      result of #eval

       kl.secure_it(obj)

      now this is the responsibilty to the user to make the "right" thing
      and at least it will not be the fault of *your* module if the user
      has a problem.

It's evident that all this must be done at $SAFE >=4

Guy Decoux

We really don't use the same ruby

svg% cat b.rb
#!/usr/bin/ruby
class EvilString < String
end

obj = EvilString.new("hello")
res = case obj
      when String
         String.new(obj)
      else
         raise "EvilString"
      end
p res
svg%

svg% b.rb
"hello"
svg%

`case' will call Module#===, try to modify it with $SAFE >= 4

Guy Decoux

···

On Fri, 2004-09-03 at 17:08 +0900, ts wrote:

1) if you use `case', you can trust the result (i.e. an object can't "lie")

Except that by using case, I can have an EvilString < String, for which
that returns true.

    - when the class of the result respond to this method,

         and is in the array

Guy Decoux