2

So I'm building a small app where you can evaluate some pieces of JavaScript code, but I'm having a huge "moral" problem:

Initially I wanted to use eval, but I found out about its dangers, so I quickly looked for an alternative.

The closest thing I could find was the function constructor, but for one thing it doesn't evaluate simple pieces of code, such as 2 + 3, since it needs a return statement, whereas eval doesn't, and it's also not that much better security-wise than eval (at least from what I've gathered).

Are there any other ways to evaluate a string as if it were code?

Corrado
  • 645
  • 1
  • 6
  • 16
  • 3
    The function constructor has the same security issues as `eval`. Anything that takes a string of untrusted JavaScript and runs it potentially has the same security issue. – Alexander O'Mara Jun 20 '17 at 17:42
  • Possible duplicate of [Is Javascript eval() so dangerous?](https://stackoverflow.com/questions/13167403/is-javascript-eval-so-dangerous) – Claies Jun 20 '17 at 17:45
  • 1
    Not really, I'm not asking if it IS dangerous, I'm saying I already know it is, and I'm looking for alternatives – Corrado Jun 20 '17 at 17:46
  • 2
    What does "simple pieces of code" mean? Is there a certain format, or is all of JavaScript allowed, including `location = "www.google.com"`, `alert('hi')`, `while (true);` `document.body.innerHTML = 'I waz here'`? – trincot Jun 20 '17 at 17:48
  • Write a JavaScript interpreter in JavaScript. Problem solved. – tadman Jun 20 '17 at 17:53
  • read *carefully* the proposed duplicate, and the duplicate that it is linked to. This is a **common** question that has been discussed ad nauseum, and you haven't asked anything new here that wasn't asked in either of those questions, and the answer provided here is the same thing that was suggested in those questions as well. – Claies Jun 20 '17 at 17:56

3 Answers3

2

If you want to evaluate JavaScript code, use eval. Is it dangerous? Yes. But that's only because evaluating JavaScript is dangerous. There's no safe way to evaluate JavaScript. If you want to evaluate JavaScript, use eval.

Take every security precaution possible. It's impossible to know what security precautions you should take without knowing more details on what you want to support and how you plan to implement it.

This may be useful:

Is It Possible to Sandbox JavaScript Running In the Browser?

https://github.com/google/caja

Goose
  • 4,764
  • 5
  • 45
  • 84
  • 1
    *"There's no safe way to evaluate JavaScript."* Sure there is. Sandboxing done right is relatively safe, unless an exploit is found. – Alexander O'Mara Jun 20 '17 at 17:46
  • @AlexanderO'Mara Yeah, but implementing a sandbox the right way in JS is hard if not impossible. – Bergi Jun 20 '17 at 18:06
  • @Bergi not really, its actually rather simple. Just see my snippet. – Jack G Sep 22 '17 at 21:28
  • @lolzerywowzery So you claim to have found a simple way to build a sandbox from which it is hard to break out? What do you want me to use for a counterexample, an `alert` maybe? – Bergi Sep 23 '17 at 10:43
1

You can easily make your own interpreter of JS in JS. I made such thing for www.Photopea.com (File - Scripts, I want to let users execute scripts over PSD documents).

Acorn is an advanced JS parser, which takes a string (JS code) and returns a syntax tree. Then, start at the root of the syntax tree and execute commands one by one.

"Jump" across the tree recursively. Use the JS call stack of the environment as a call stack of the interpreted code. Use JS objects {var1: ..., var2: ...} to store values of variables in each execution space (global, local in a function ...).

You can allow that code to access data from the outer environment through some interface, or make it completely sandboxed. I thought that making my own interpreter would take me a week, but I made it like in 6 hours :)

Ivan Kuckir
  • 2,327
  • 3
  • 27
  • 46
0

Please never ever use eval no matter what, there is a much better alternative. Instead of eval, use new function. eval is evil, there's no question about that, but most people skip over the most evil aspect of eval: it gives you access to variables in your local scope. Back in the 90's, back before the concept of JIST compilation, eval's sounded like a good idea (and they were): just insert some additional lines dynamically into the code you're already executing line-by-line. This also meant that evals didn't really slow things down all that much. However, now-a-days with JIST compilation eval statements are very taxing on JIST compilers which internally remove the concept of variable names entirely. For JIST compilers, in order to evaluate an eval statement, it has to figure out where all of its variables are stored, and match them with unknown globals found in the evaled statement. The problem extends even deeper if you get really technical.

But, with new function, the JIST compiler doesn't have to do any expensive variable name lookups: the entire code block is self-contained and in the global scope. For example, take the following terribly inefficient eval snippet. Please note that this is only for the purpose of being an example. In production code, you shouldn't even be using eval or new Function to generate a function from a string whose content is already known.

var a = {
    prop: -1
};
var k = eval('(function(b){return a.prop + b;})');
alert( k(3) ); // will alert 2

Now, let's take a look at the much better new Function alternative.

var a = {
    prop: -1
};
var k = (new Function('a', 'b', 'return a.prop + b')).bind(undefined, a);
alert( k(3) ); // will alert 2

Notice the difference? There is a major one: the eval is executed inside the local scope while the new Function is executed inside the global one.

Now, onto the next problem: security. There is a lot of talk about how security is difficult, and yes, with eval it is pretty much impossible (e.x. if you wrap the whole code in a sandboxing function, then all you have to do is prematurely end the function and start a new one to execute code freely in the current scope). But, with new Function, you can easily (but not the most efficiently) sandbox anything. Look at the following code.

var whitelist = ['Math', 'Number', 'Object', 'Boolean', 'Array'];
var blacklist = Object.getOwnPropertyNames(window).filter(function(x){
    return whitelist.indexOf(x) === -1 && !/^[^a-zA-Z]|\W/.test(x)
});
var listlen = blacklist.length;
var blanklist = (new Array(listlen+1)).fill(undefined);
function sandboxed_function(){
    "use-strict";
    blacklist.push.apply(blacklist, arguments);
    blacklist[blacklist.length-1] = 
        '"use-strict";' + arguments[arguments.length-1];
    var newFunc = Function.apply(
        Function,
        blacklist
    );
    blacklist.length = listlen;
    return newFunc.bind.apply(newFunc, blanklist);
}

Then, fiddle around with the whitelist, get it just the way you want it, and then you can use sandboxed_function just like new Function. For example:

var whitelist = ['Math', 'Number', 'Object', 'Boolean', 'Array'];
var blacklist = Object.getOwnPropertyNames(window).filter(function(x){
    return whitelist.indexOf(x) === -1 && !/^[^a-zA-Z]|\W/.test(x)
});
var listlen = blacklist.length;
var blanklist = (new Array(listlen+1)).fill(undefined);
function sandboxed_function(){
    "use-strict";
    blacklist.push.apply(blacklist, arguments);
    blacklist[blacklist.length-1] = 
        '"use-strict";' + arguments[arguments.length-1];
    var newFunc = Function.apply(
        Function,
        blacklist
    );
    blacklist.length = listlen;
    return newFunc.bind.apply(newFunc, blanklist);
}
var myfunc = sandboxed_function('return "window = " + window + "\\ndocument = " + document + "\\nBoolean = " + Boolean');
output.textContent = myfunc();
<pre id="output"></pre>

As for writing code to be runned under this strict sandbox, you may be asking, if window is undefined, how do I test for the existence of methods. There are two solutions to this. #1 is just simply to use typeof like so.

output.textContent = 'typeof foobar = ' + typeof foobar;
<div id="output"></div>

As you can see in the above code, using typeof will not throw an error, rather it will only just return undefined. The 2nd primary method to check for a global is to use the try/catch method.

try {
    if (foobar)
        output.textContent = 'foobar.constructor = ' + foobar.constructor;
    else
        output.textContent = 'foobar.constructor = undefined';
} catch(e) {
    output.textContent = 'foobar = undefined';
}
<div id="output"></div>

So, in conclusion, I hope my code snippets gave you some insight into a much better, nicer, cleaner alternative to eval. And I hope I have aspired you to a greater purpose: snubbing on eval. As for the browser compatibility, while the sandboxed_function will run in IE9, in order for it to actually sandbox anything, IE10+ is required. This is because the "use-strict" statement is very essential to eliminating much of the sneaky sand-box breaking ways like the one below.

var whitelist = ['Math', 'Number', 'Object', 'Boolean', 'Array'];
var blacklist = Object.getOwnPropertyNames(window).filter(function(x){
    return whitelist.indexOf(x) === -1 && !/^[^a-zA-Z]|\W/.test(x)
});
var listlen = blacklist.length;
var blanklist = (new Array(listlen+1)).fill(undefined);
function sandboxed_function(){
    blacklist.push.apply(blacklist, arguments);
    blacklist[blacklist.length-1] = 
        /*'"use-strict";' +*/ arguments[arguments.length-1];
    var newFunc = Function.apply(
        Function,
        blacklist
    );
    blacklist.length = listlen;
    return newFunc.bind.apply(newFunc, blanklist);
}
var myfunc = sandboxed_function(`return (function(){
    var snatched_window = this; // won't work in strict mode where the this
                                // variable doesn't need to be an object
    return snatched_window;
}).call(undefined)`);
output.textContent = "Successful broke out: " + (myfunc() === window);
<pre id="output"></pre>
One last final comment is that if you are going to allow event API's into your sandboxed environment, then you must be careful: the view property can be a window object, making it so you have to erase that too. There are several other things, but I would recommend researching thoroughly and exploring the objects in Chrome's console.
Jack G
  • 4,553
  • 2
  • 41
  • 50
  • `"use strict"` has no dash, it's called *JIT* compiler, and `blacklist`/`blanklist` are hard to distinguish – Bergi Sep 23 '17 at 10:44
  • Hi, I noticed you've [posted this answer here too](https://stackoverflow.com/a/46374395/472495). Would you delete one of them? We don't allow duplicate answers, since we think that if the exact same answer works for both, the questions must also be duplicate, and one of them can be put on hold. If the questions are not duplicate, each answer should be tailored to the question. – halfer Sep 23 '17 at 18:34