Search code examples
rubyeval

How to Make Or Reference a Null Ruby Binding For Eval


Rubocop dislikes the following; it issues Pass a binding, __FILE__ and __LINE__ to eval.:

sort_lambda = eval "->(a) { a.date }"

Yes, I know that eval is a security problem. The issue of security is out of scope for this question.

The Ruby documentation on binding says:

Objects of class Binding encapsulate the execution context at some particular place in the code and retain this context for future use. The variables, methods, value of self, and possibly an iterator block that can be accessed in this context are all retained. Binding objects can be created using Kernel#binding, and are made available to the callback of Kernel#set_trace_func and instances of TracePoint.

These binding objects can be passed as the second argument of the Kernel#eval method, establishing an environment for the evaluation.

The lambda being created does not need to access any variables in any scopes. A quick and dirty binding to the scope where the eval is invoked from would look like this:

sort_lambda = eval "->(a) { a.date }", self.binding, __FILE__, __LINE__

Ideally, a null binding (a binding without anything defined in it, nothing from self, etc.) should be passed to this eval instead. How could this be done?


Solution

  • Not exactly, but you can approximate it.

    Before I go further, I know you've already said this, but I want to emphasize it for future readers of this question as well. What I'm describing below is NOT a sandbox. This will NOT protect you from malicious users. If you pass user input to eval, it can still do a lot of damage with the binding I show you below. Consult a cybersecurity expert before trying this in production.

    Great, with that out of the way, let's move on. You can't really have an empty binding in Ruby. The Binding class is sort of compile-time magic. Although the class proper only exposes a way to get local variables, it also captures any constant names (including class names) that are in scope at the time, as well as the current receiver object self and all methods on self that can be invoked from the point of execution. The problem with an empty binding is that Ruby is a lot like Smalltalk sometimes. Everything exists in one big world of Platonic ideals called "objects", and no Ruby code can truly run in isolation.

    In fact, trying to do so is really just putting up obstacles and awkward goalposts. Think you can block me from accessing BasicObject? If I have literally any object a in Ruby, then a.class.ancestors.last is BasicObject. Using this technique, we can get any global class by simply having an instance of that class or a subclass. Once we have classes, we have modules, and once we have modules we have Kernel, and at that point we have most of the Ruby built-in functionality.

    Likewise, self always exists. You can't get rid of it. It's a fundamental part of the Ruby object system, and it exists even in situations where you don't think it does (see this question of mine from awhile back, for instance). Every method or block of code in Ruby has a receiver, so the most you can do is try to limit the receiver to be as small an object as possible. One might think you want self to be BasicObject, but amusingly there's not really a way to do that either, since you can only get a binding if Kernel is in scope, and BasicObject doesn't include Kernel. So at minimum, you're getting all of Kernel. You might be able to skimp by somehow and use some subclass of BasicObject that includes Kernel, thereby avoiding other Object methods, but that's likely to cause confusion down the road too.

    All of this is to emphasize that a hypothetical null binding would really only make it slightly more complicated to get all of the global names, not impossible. And that's why it doesn't exist.

    That being said, if your goal is to eliminate local variables and to try, you can get that easily by creating a binding inside of a module.

    module F
      module_function def get_binding
        binding
      end
    end
    
    sort_lambda = eval "->(a) { a.date }", F.get_binding
    

    This binding will never have local variables, and the methods and constants it has access to are limited to those available in Kernel or at the global scope. That's about as close to "null" as you're going to get in the complex nexus of interconnected types and names we call Ruby.