Bill Kelly wrote:
Sandboxing is made nearly impossible if calls in your own chain can escalate privileges by executing code from your binding.
Since you mention below JRuby doesn't support $SAFE, then I'm at a
loss as to which sandboxing mechanism you refer. (Does JRuby offer an
alternative sandboxing approach?)
The JVM has its own built-in security mechanisms, and we do nothing to override or subvert them. We've considered adding portions of $SAFE like limiting eval and loading of code from the filesystem, but largely $SAFE is so amorphous and untested we've opted to leave it basically unimplemented. Also, a substantial portion of $SAFE's guarantees come from tainting, which requires checking taint on objects over and over and over again. I don't think it's a realistic or reliable security mechanism as it exists today.
It seems to me there's trusted code, and untrusted code. We allow trusted
code to do all sorts of potentially dangerous things, including reopening and
redefining any method in any class, etc. etc. So again, if we're talking about
trusted code, I'm hard pressed to see the validity of holding Binding.of_caller
up as an exemplar of something somehow more dangerous than the myriad
privileges already granted to trusted code in ruby.
Without Binding.of_caller, no trusted or untrusted Ruby code can access a method's local variables unless that method passes a block or binding explicitly. of_caller opens up every method to that possibility. You don't think that exposure is problematic?
Conversely, if we're talking about untrusted code, Binding.of_caller seems
a relatively unremarkable addition to an already existing great number of
avenues by which untrusted code can affect the system unless (somehow)
sandboxed.
It goes well above and beyond existing mechanisms since it exposes details of a caller's bindings that would never be accessible through any other means.
Similar logic kicked retry out of 1.9, since it caused the original receiver and arguments to be reevaluated, whether the caller that behavior or not.
In general we should avoid adding the potential for locally-scoped side effects that no amount of inspection can detect. It opens up a big can of worms if you can't look at a method and know each step along the way that the variables are going to be what you set them do, and that no call you make will invalidate that expectation.
There's also logistical concerns. If you had Binding.of_caller available, how do you ensure you're not irreparably damaging the caller's context?
def do_call
binding = Binding.of_caller
eval "some_local_var = nil", binding
end
def innocent
some_local_var = important_value
do_call
# now, without me assigning it, some_local_var has been changed
end
This is a horrible breach of trust that should never, ever be made possible in Ruby.
It's thread local. And it apparently somehow does get localized to a
given binding (?) because ruby remembers the $SAFE level at which a
lexical closure was compiled (or at least, a proc.)
For example, if a thread is at $SAFE=4, and calls a proc which was
compiled at $SAFE=0, then the proc executes at $SAFE=0.
I do share your concerns about $SAFE never having been formally
audited for security, and I'd agree it would be foolish to assume there
don't exist some undiscovered holes in the system.
(On the other hand, in practical terms, we live with as yet undiscovered
security holes in windows, linux, IIS, apache, etc. every day too.)
$SAFE is one place where we simply can't get by with the loose specifications in place for the rest of Ruby. I'd welcome discussions on how to make $SAFE or something similar a formally-specified security mechanism for Ruby. But as it stands now, I think it's too vague and requires too many little bits of code sprinkled all over the codebase to function correctly. I'm no security expert, but relying on a million tiny little taint checks to ensure the sandbox remains secure seems totally infeasible. If you have just *one* core method that doesn't propagate tainting correctly, you're screwed.
That said, it appears $SAFE=4 was designed with the _intent_ to
provide a secure sandbox. And it seems to me the $SAFE mechanism
could as easily disallow inappropriate uses of Binding.of_caller, just
as it currently disallows the reopening of classes and modules, etc.
You're right about $SAFE locality...a binding does appear to keep a reference to its original thread's state, so it sees that thread's $SAFE. But of course that's not really applicable here, since $SAFE propagates down normal call stacks, and Binding.of_caller is intended for normal call stacks. So caller and callee would always have the same $SAFE level either way.
If you're talking about doing Binding.of_caller from within a proc body that came from another thread that has a different $SAFE level, with the caller calling Proc#call and the callee using that call to get the caller's binding with Binding.of_caller...I think we've probably got other complexity and threading issues to consider before we try to secure Binding.of_caller.
Here's an example of what you'd have to do to have a different caller $SAFE than callee:
puts "safe outside: #{$SAFE}"
x = nil
Thread.new { x = proc { puts "safe in proc: #{$SAFE}" } }.join
$SAFE = 1
puts "safe escalated: #{$SAFE}"
x.call
new_safe = eval "$SAFE", x
puts "safe from binding #{new_safe}"
This outputs 0, 1, 0, 1 as you'd expect. So the only way you'd potentially have a different safe level for a caller than for a callee is if you were evaluating code against a binding with a different save level (at which point we're explicitly using a binding anyway) or calling a proc (where it would be pretty rare to see Binding.of_caller). So I think $SAFE is not a particularly useful way to secure individual frames in the call stack.
Again, I don't see why Binding.of_caller is particularly sharper than many
other knives in ruby. (If I try to imagine writing malicious code I know will
be executed with full privileges, Binding.of_caller isn't the first tool that
comes to mind when I contemplate all the nefarious possibilities.)
It's WAY sharper because it invalidates infallible truths about execution flow in a method body. Today, you can be guaranteed that unless you explicitly pass a binding to another call (either through eval/binding or by passing a block) none of your local variables will be mutated unless you mutate them yourself. Binding.of_caller breaks that assumption for all code everywhere. I don't want to live in that world.
- Charlie