Bill K. 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.
(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.