5

I'm trying to run trusted JS code in an "isolated" context. Basically came up with this method:

function limitedEval(src, context) {
    return (function() {
        with(this) {
            return eval(src) 
        }
    }).call(context)
}

This works great, however when the script is using the var keyword it is stored in the execution context as opposed to the provided context in the with statement (which I understand is by design). So for example, the following code doesn't work:

var ctx = {};
limitedEval('var foo = "hello"', ctx);
limitedEval('alert(foo)', ctx); // error: foo is undefined

I'd like to be able to call limitedEval() multiple times and reuse the context. Is that possible?

Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
Dandan
  • 519
  • 2
  • 9
  • 4
    "Trusted" and `eval()` never belong in the same discussion. – Scott Marcus May 15 '17 at 22:28
  • Can you elaborate? – Dandan May 15 '17 at 22:35
  • he means that eval is not a secured function and can introduce multiple security issues. You should be looking into something like https://github.com/ternjs/acorn – Dayan Moreno Leon May 15 '17 at 22:38
  • There really are extremely limited use cases for `eval()`. It should generally be avoided. – Scott Marcus May 15 '17 at 22:41
  • 1
    @ScottMarcus They belong in exactly the same discussion - using `eval` *requires* that the executed code is trusted. – Bergi May 15 '17 at 22:46
  • 1
    You might be interested in [this solution^H^H^H^H^H hack](http://stackoverflow.com/a/41050254/1048572) – Bergi May 15 '17 at 22:49
  • What environment are you running this in? Node has the [vm module](https://nodejs.org/api/vm.html) for *exactly* this purpose – Bergi May 15 '17 at 22:50
  • Browser environment. Global context is the window object. – Dandan May 15 '17 at 23:09
  • `with` and `eval` in the same function... Do you want to upset Douglas Crockford? – Badacadabra May 15 '17 at 23:23
  • For browser environment webworkers may be a way to do this. You can simply create an eval service with a webworker and run code in a separate thread. – slebetman May 16 '17 at 00:17
  • Got it working, but definitely feels like a hack: https://jsfiddle.net/2g0nkf7z/7/ – Dandan May 16 '17 at 00:33
  • @Dandan That's because `alert` does not directly refer to the global variable, but is accessed as a property of the proxy, and is called as a method on it - leading to an error. `alert.call(null, hello)` or `console.log(hello)` work as they're not plain calls. In your case you probably want to restrict access to global variables anyway so I don't see an issue with that, but yes it's just a hack and will need more hacking to work as expected… – Bergi May 16 '17 at 00:35

1 Answers1

5

This seems like a very interesting problem. The problem with your code is that you generate new function every time you execute limitedEval. This means that whatever variables you create using var keyword, will only exist within the context of that function. What you really need is to have 1 function per context and reuse that function's context. The most obvious way to do that is by using generators. Generators are essentially functions that can be paused and then restarted.

// IIFE to store gen_name
var limitedEval = function() {
    // Symbol for generator property so we don't pollute `ctx` namespace
    var gen_name = Symbol();

    return function limitedEval(src, context) {
        if(!(gen_name in context)) {
            // Generator that will run eval and pause til getting the next source code
            var generator = function * () {
                with(this) {
                    while(true) {
                        yield eval( yield );
                    }
                }
            };

            context[gen_name] = generator.call(context);

            // Initially, we need to execute code until reaching the first `yield`
            context[gen_name].next();
        }

        // First, send the source to the `eval`
        context[gen_name].next( src );

        // Second, get the `eval` result
        return context[gen_name].next().value;
    };
}();

And now you can call

var ctx = {};
limitedEval('var foo = "hello"', ctx);
limitedEval('alert(foo);', ctx);

Every limitedEval call will now reuse whatever generator function it will find on the provided ctx object. If the function doesn't exist, it will create it and put it on the ctx object. Because you now have only one function per ctx, it will reuse the function's context when creating variables with var keyword. Note: you will not be able to look up those variables via the ctx object, because they will only exist within the function's context.

I'm not entirely sure if you can achieve the same result without using generators.

Edit: others made great suggestions so I replaced randomly generated property with Symbol and did with(this) instead of with(context)

guitarino
  • 348
  • 1
  • 7
  • 1
    You should use a `Symbol()` to prevent polluting the context namespace! – Bergi May 16 '17 at 00:43
  • 1
    I'd recommend to make the generator not an IIFE but a named generator function, and to use `with(this)` and `yield eval(yield)` to avoid any scope variables in the `eval` – Bergi May 16 '17 at 00:48
  • Bonus question: is there a way to achieve this in strict mode? with keyword is restricted. – Dandan May 19 '17 at 07:22
  • 1
    i really don't see how this is supposed to created a limited context. `with` keyword does not restrict access to the parent scope. It just looks up names in the context first. You may want to use a `Proxy` object in `with` that if the target doesn't have the property it just returns `undefined` – toraman Jan 31 '22 at 00:08