Forum: Ruby Why doesn't Ruby have a built in sandbox class?

Posted by Ano Hito (william_b89)
on 2013-02-10 10:44
After having some trouble getting the j-ruby sandbox gem to work
(http://www.ruby-forum.com/topic/4410708), and giving some thought to
the idea of writing a pure ruby implementation of the sandbox (a more or
less unworkable idea), a thought occurred. Why doesn't ruby ship with a
sandbox class?

I know ruby has safe levels, and safe levels do work for many scenarios
in which you wish to run potentially unsafe code. But they really don't
offer the same fine grained control that a true sandbox does. There's
just something really appealing about the idea of:

class A

  def initialize
    B.new
  end

end

class B

  def initialize
    puts "test"
  end

end

box = Sandbox.new
box.ref A

box.eval("a = A.new") #"test"
box.eval("b = B.new") #fail: I don't know what a "B" is

This is simple, elegant, and far better than setting "$SAFE = 4" and
hoping for the best. It's also the model Why used for his Freaky Sandbox
project. Why wrote his sandbox more than 6 years ago in the hopes that
it would make ruby a more useful language. Unfortunately his
implementation was rather hacky and difficult to install, leaving much
room for improvement. But here we are, more than 6 years later, and the
options for sandboxing ruby still suck.

The current option are as follows:
$SAFE = 4: Works well for cases where you just want to keep bad things
from happening, but offers no real way to specify what objects code can
or can't access.
jruby-sandbox: Works well, but of course requires jruby which is not
always an option.
Why's Freaky Sandbox: Sure, just keep using ruby 1.8.6 forever and
you're set. Why not?
shikashi (https://github.com/tario/shikashi): I couldn't even get it to
work on my system (C extensions wouldn't compile) so I can't comment on
it's effectiveness. However, after examining the source code for a bit,
I think the implementation is misguided at best. I can't entirely blame
the dev though because the truth of the matter is...

Sandboxing is not something that should be left to a third party to
develop. It belongs in the core of the language if it belongs anywhere
at all. So why isn't it? Personally I have a few theories.

It could be that the ruby devs think safe levels are good enough. I
would buy this, except safe levels are a very inelegant and unrubylike
solution to a problem for which a much simpler solution could exist.
Need to mess with something unsafe? Just throw it in the box. Why use
constructs like "taintedness" when you could just throw them out in
favor of something more effective and flexible, but less complicated.

It could also be that the ruby devs are convinced that the third party
offerings are already satisfying the demand for a ruby sandbox. I think
you could make this argument for the case of jruby, but for any and all
other ruby implementations out there, the truth is, no, no they aren't.

But maybe they think nobody needs or even wants sandboxing? I'm sure a
lot of people don't, but sometimes you never know how useful something
can be until you have the option of using it. As much as sandboxing
could become a bad solution to many common problems, it is still the
only solution to a set of less common problems. Personally I think the
number of problems that are unsolvable with a language is always
something worth reducing. Especially if it might also serve to simplify
things that are currently implemented in a complex way.

That leads me to my last theory, maybe there is just no good way to
implement a sandbox in ruby without it becoming an ugly hack? I can't
give a definitive answer to this question because I am not as familiar
with ruby's internals as I would like to be. What I can tell you is that
at the very least, if I was building a ruby implementation from scratch,
I know how I'd do it.

Here's how it would work. As opposed to having a single object space for
everything, we allow for the creation of multiple object spaces
(ObjectSpace.new). Every thread would then be attached to the object
space it was created it, so that anything you did in that thread would
exist in a separate world from anything happening in another object
space. You could use objectspace.eval to make stuff happen in any object
space for which you have a reference. If you want to put something in an
object space, you can call objectspace.ref a_thing, and it now exists in
that object space too. Sort of...

To implement this properly you'd need a wrapper class (SharedObject).
Then when you called sandboxspace.ref object, it would create a instance
of a wrapper for the object that would exist only in sandboxspace, but
would use the same object id of the original object. The wrapper would
contain only a single send method which would temporarily switch the
objectspace of the current thread (an ability that would of course not
be allowed for normal ruby code), create wrappers for any objects being
sent as parameters, and then send them to the method being called.
Incidentally, due to the overhead involved in this it may be desirable
to allow for duplicating certain class definitions in an object space as
opposed to using wrappers. I don't really think you want to have to
switch spaces every time you create a string, for example.

I think this would be a very good approach that would offer ruby a much
more flexible security model that it currently has. If only I had the
time or experience with ruby's code base to implement it... All I can do
is offer my approach as a suggestion. But maybe it will get the ball
rolling, get people talking, that kind of stuff. Maybe someone will
think of a much better approach to implementing sandboxes than I ever
could. Frankly, I don't know exactly how much work my solution would be
to implement. It could be a weekend project for an experienced ruby dev,
or it could be a massive three month rewrite of tons of mission critical
code. Either way, it would be an improvement to ruby, so I hope someone
at least tries. Remember, you'll never be able to justify calling it
ruby 2.0 unless you have lots of cool new features to play with. ;)
Posted by Robert Klemme (robert_k78)
on 2013-02-10 21:14
(Received via mailing list)
On Sun, Feb 10, 2013 at 10:45 AM, Ano Hito <lists@ruby-forum.com> wrote:

> Why doesn't ruby ship with a sandbox class?
>
> I know ruby has safe levels, and safe levels do work for many scenarios
> in which you wish to run potentially unsafe code. But they really don't
> offer the same fine grained control that a true sandbox does.

Maybe, that fine grained control you are thinking of is too
complicated to realize - or does not even exist.

> box.eval("a = A.new") #"test"
> box.eval("b = B.new") #fail: I don't know what a "B" is
>
> This is simple, elegant, and far better than setting "$SAFE = 4" and
> hoping for the best.

You make it sound as if people who use $SAFE would not know exactly
how it works.

> Sandboxing is not something that should be left to a third party to
> develop. It belongs in the core of the language if it belongs anywhere
> at all. So why isn't it? Personally I have a few theories.
>
> It could be that the ruby devs think safe levels are good enough. I
> would buy this, except safe levels are a very inelegant and unrubylike
> solution to a problem for which a much simpler solution could exist.
> Need to mess with something unsafe? Just throw it in the box. Why use
> constructs like "taintedness" when you could just throw them out in
> favor of something more effective and flexible, but less complicated.

It remains to be seen whether the alternatives are really simpler.

> But maybe they think nobody needs or even wants sandboxing? I'm sure a
> lot of people don't, but sometimes you never know how useful something
> can be until you have the option of using it. As much as sandboxing
> could become a bad solution to many common problems, it is still the
> only solution to a set of less common problems.

I am not a regular user of $SAFE and at the moment I have no idea what
use cases you are referring to here.  Can you share some more detail,
please?

> Personally I think the
> number of problems that are unsolvable with a language is always
> something worth reducing.

I wouldn't say always.  All changes come with a cost.  If the cost is
too high then the reduction of unsolvable problems is not worthwhile.

> Especially if it might also serve to simplify
> things that are currently implemented in a complex way.

If that is the case, then, yes, I agree.

> space for which you have a reference. If you want to put something in an
> object space, you can call objectspace.ref a_thing, and it now exists in
> that object space too. Sort of...
>
> To implement this properly you'd need a wrapper class (SharedObject).
> Then when you called sandboxspace.ref object, it would create a instance
> of a wrapper for the object that would exist only in sandboxspace, but
> would use the same object id of the original object. The wrapper would
> contain only a single send method which would temporarily switch the

Did you mean #method_missing instead of #send?

> objectspace of the current thread (an ability that would of course not
> be allowed for normal ruby code), create wrappers for any objects being
> sent as parameters, and then send them to the method being called.

I think there are some issues with wrappers.  Assuming object X lives
in object space A and was thrown into objectspace B.  Y and Z are
other objects private to object space B.

1. A method call from Y to X which passes Z as argument would
implicitly export Z to A.  For a sandbox it would not make sense to
_implicitly_ export something to another box (object space).

2. How far in the object graph starting at Z do you want to go with
wrapper creation?  It's not that you only export Z to A but also a
whole lot other objects.  This is not only a security issue (item 1)
but also a performance issue because you would need a recursive wrap
of all objects.

3. How do you ensure objects stay inside their boxes if you
automatically create wrappers for method arguments?  It seems the
situation is not that different as with a single global object space.

The situation would be easier if you allowed for object graphs
starting at method arguments only objects which are known to the
target object space, i.e. disallow implicit migration.  I am not so
sure though whether that solution is so far away from $SAFE - or
creating separate processes which communicate via DRb.

> I think this would be a very good approach that would offer ruby a much
> more flexible security model that it currently has. If only I had the
> time or experience with ruby's code base to implement it... All I can do
> is offer my approach as a suggestion. But maybe it will get the ball
> rolling, get people talking, that kind of stuff.

I believe your suggestion certainly needs more thinking.  Before we
talk about implementations we should clarify requirements - even more
so as this is a security topic.

> Remember, you'll never be able to justify calling it
> ruby 2.0 unless you have lots of cool new features to play with. ;)

I am pretty sure it's too late to include such a fundamental
architectural change into Ruby 2.0.  That sounds more like a Ruby 3.0
thing.

Kind regards

robert
Posted by Robert Klemme (robert_k78)
on 2013-02-10 21:15
(Received via mailing list)
On Sun, Feb 10, 2013 at 10:45 AM, Ano Hito <lists@ruby-forum.com> wrote:
> After having some trouble getting the j-ruby sandbox gem to work
> (http://www.ruby-forum.com/topic/4410708), and giving some thought to
> the idea of writing a pure ruby implementation of the sandbox (a more or
> less unworkable idea), a thought occurred. Why doesn't ruby ship with a
> sandbox class?

PS: I forgot one thing: thank you for starting that interesting 
discussion!
Posted by Ano Hito (william_b89)
on 2013-02-11 07:14
Robert Klemme wrote in post #1096190:
> You make it sound as if people who use $SAFE would not know exactly
> how it works.
Not so much that they wouldn't know how it works, but that it may 
provide a false sense of security. Just look at this 
(http://blog.segment7.net/2006/08/30/reducing-safe) and this 
(http://www.h-online.com/open/news/item/Multiple-vu...). 
Safe levels work by blacklisting things you don't want to be available 
to untrusted code. The problem with this is the same problem with any 
type of blacklisting, it violates the principle of least privilege 
(http://en.wikipedia.org/wiki/Principle_of_least_privilege). While it 
may be nice to think you can be secure by not letting code have access 
to things that could be dangerous, it is practically impossible to 
predict what is or isn't dangerous in every given scenario. It's always 
a better idea to use an approach that allows untrusted code access to 
what it needs to function, and only what it needs to function. Safe 
levels don't do that.

> I am not a regular user of $SAFE and at the moment I have no idea what
> use cases you are referring to here.  Can you share some more detail,
> please?
Why himself actually listed several cases in the original README for his 
sandbox:

 * Start small interpreters which can periodically be dumped and 
restarted.
   (Long-running apps can self-upgrade!)

 * Load multiple different versions of a library.  Maybe you're 
converting
   an old version to a new version.

 * Run multiple Rails apps.  (Rails is a classic example of a crowded 
namespace
   where two apps' set of modules cannot coexist.)

I would additionally add to that the ability to run your code under a 
hypervisor for added security, even in cases where you trust the code 
you're running. And of course, that I believe sandboxes are in general, 
more secure than blacklisting.

> Did you mean #method_missing instead of #send?
I could be wrong about this, but I think send is the only method a ruby 
object is actually required to have. Otherwise you couldn't call on the 
object to do anything. You could use method_missing, but you'd still 
need to have a send method, because I'm pretty sure method_missing only 
gets called in the event that the send method fails.

> I think there are some issues with wrappers.  Assuming object X lives
> in object space A and was thrown into objectspace B.  Y and Z are
> other objects private to object space B.
>
> 1. A method call from Y to X which passes Z as argument would
> implicitly export Z to A.  For a sandbox it would not make sense to
> _implicitly_ export something to another box (object space).
Wouldn't it? I think that you have to keep in mind that most of what 
you're doing in a sandbox is interacting with the stuff already in the 
box. In cases where you allow outside stuff to go in, you should 
generally keep it restricted to objects that act as controlled 
interfaces between what's outside the box and what's inside the box. 
That being said in order to prevent massive unintentional leaks of 
objects into a box from occurring, it would probably be a good idea to 
make it so you could only access the methods of an object exported to a 
box if it's class had also already been exported.

> 2. How far in the object graph starting at Z do you want to go with
> wrapper creation?  It's not that you only export Z to A but also a
> whole lot other objects.  This is not only a security issue (item 1)
> but also a performance issue because you would need a recursive wrap
> of all objects.
If you're referring to doing something like wrapping all the objects 
referenced to by the instance variable of objects being wrapped, you 
wouldn't do that. All method calls to the object would switch the object 
space of the current thread to the object's native space, in which all 
of it's references would already be valid. Granted the context switching 
would be a drag on performance, but you wouldn't be doing it that much, 
because your general goal is to limit the interaction of things in the 
box with things outside of it.

> 3. How do you ensure objects stay inside their boxes if you
> automatically create wrappers for method arguments?  It seems the
> situation is not that different as with a single global object space.
While you don't necessarily keep objects in only a single space by doing 
this, at the least you maintain control over what they can access. Any 
reference to a foreign object, be it in the original object space or the 
one you're using for a sandbox, is always going to be wrapped. It's 
methods will always execute inside of it's native object space. Even 
when an object leaves the box, it never truly leaves the box.

> The situation would be easier if you allowed for object graphs
> starting at method arguments only objects which are known to the
> target object space, i.e. disallow implicit migration.  I am not so
> sure though whether that solution is so far away from $SAFE - or
> creating separate processes which communicate via DRb.
I'm not quite sure how what you're saying would work. Could you 
elaborate a bit?

> I believe your suggestion certainly needs more thinking.  Before we
> talk about implementations we should clarify requirements - even more
> so as this is a security topic.
It couldn't hurt. I mean, if someone does seriously consider 
implementing sandboxing as a native feature of ruby, the last thing I'd 
want is for it to turn into a security disaster that gets scrapped 
before it ever hits a stable release. I will however say that sandboxing 
is a tried an true approach to securely executing code. My version of 
sandboxing is slightly different from the typical because I was trying 
to think of something that would be more in line with the way ruby 
already works, and not something that would require dozens of hacks to 
implement. If any ruby devs want to jump in here and tell me all the 
reasons a more traditional sandbox would be a million times easier to 
implement, I'm all ears.

> I am pretty sure it's too late to include such a fundamental
> architectural change into Ruby 2.0.  That sounds more like a Ruby 3.0
> thing.
Is ruby 2.0 getting close to release then? I honestly haven't been 
following ruby's development closely enough to keep up on these things, 
but I was assuming 2.0 was still at least a year away. Well, while 
implementing native sandboxing may require some architectural 
reshuffling, it wouldn't require any changes to the ruby environment 
itself that wouldn't be more or less backward compatible with all 
existing code. The only thing I could think of is if you'd written code 
that specifically depended on ObjectSpace not having a new method, but 
I'm not sure that would come up very often. Maybe someone could still 
sneak sandboxes into 2.0, you never know.

>PS: I forgot one thing: thank you for starting that interesting
>discussion!
My pleasure. ;) I've been a long time user of ruby for my personal 
scripting needs, but I've never really done much as far as contributing 
to the greater ruby scene. Well, there's this 
(https://github.com/An0Hit0/Ruby-ISO-9660), which is useful, despite 
being unfinished. Anyway, I'd like to contribute a bit more, but after 
looking at the ruby source code, it became apparent it would take more 
time than I have to familiarize myself with the code enough that I could 
even approach the issue of implementing sandboxes, let alone actually 
doing it.
Posted by Robert Klemme (robert_k78)
on 2013-02-11 09:23
(Received via mailing list)
On Mon, Feb 11, 2013 at 7:14 AM, Ano Hito <lists@ruby-forum.com> wrote:
> Robert Klemme wrote in post #1096190:

> that specifically depended on ObjectSpace not having a new method, but
> looking at the ruby source code, it became apparent it would take more
> time than I have to familiarize myself with the code enough that I could
> even approach the issue of implementing sandboxes, let alone actually
> doing it.

I don't have enough time to follow up on the rest of the discussion
but with regard to timing: 2.0 is RC already and with what you say
above you acknowledge that architectural changes of MRI are a major
effort.

http://www.ruby-lang.org/en/news/2013/02/08/ruby-2...

Kind regards

robert
Posted by Ano Hito (william_b89)
on 2013-02-11 09:49
Robert Klemme wrote in post #1096227:
> I don't have enough time to follow up on the rest of the discussion
> but with regard to timing: 2.0 is RC already and with what you say
> above you acknowledge that architectural changes of MRI are a major
> effort.
>
> http://www.ruby-lang.org/en/news/2013/02/08/ruby-2...

Ah, ok, things are quite far along. 3.0 it is then. Incidentally, this 
bullet point is relevant to my interests regarding the project I was 
originally inquiring about in my other thread: "NativeClient support". 
That may prove to be a better option for my project than jruby, as 
NativeClient modules are already loaded in a heavily protected sandbox, 
which lets me be a little more comfortable relying on safe levels. Then 
again, I also just found out about this 
(http://qiezi.me/2013/01/09/webruby-1-2-3-tutorial/), which has a fair 
amount of advantages, such as not requiring chrome, and having a very 
snappy load time. Nice to know there are getting to be more options for 
running ruby in the browser.
Posted by Tony Arcieri (Guest)
on 2013-02-12 06:49
(Received via mailing list)
I posted a strawman for something similar a few days ago. It's a bit
different idea from yours and a super-granular class whitelist:

https://gist.github.com/tarcieri/4719525

Clearly that doesn't cover all the "NO" cases, but I hope it gives you a
general idea
Posted by Ano Hito (william_b89)
on 2013-02-12 11:37
Tony Arcieri wrote in post #1096413:
> I posted a strawman for something similar a few days ago. It's a bit
> different idea from yours and a super-granular class whitelist:
>
> https://gist.github.com/tarcieri/4719525
>
> Clearly that doesn't cover all the "NO" cases, but I hope it gives you a
> general idea

Creating a secure context inside of a block would certainly be more 
rubylike than $SAFE = 4. But would you allow for managing what classes 
and methods are allowed to be used in a SecureContext? Otherwise it's 
not that different from just running the code in a new thread after 
setting $SAFE = 4 in the thread's context.
Posted by Tony Arcieri (Guest)
on 2013-02-13 01:20
(Received via mailing list)
Wow, I wasn't aware $SAFE=4 was quite that powerful. Too bad it doesn't
work on JRuby or I might seriously consider using it ;)
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.