0

I am working on a developer tool that should evaluate user code the same way the console does. For example, if I type multiple expression into the console, like this:

var x = 0; x++; x;

I immediately get the result of the last expression (1).

The obvious solution to replicate this behavior in my web app would be to execute the user code with eval, like this:

let result = eval('var x = 0; x++; x;');

However, eval has obvious performance implications and security concerns, so I want to avoid it. My solution right now is to inject a script in a sandboxed environment (iframe). I feel like this is better than eval, but it does not solve the main issues which are:

  1. Modifying the user code in such a way that it can be written to a variable
  2. Getting the result of the last expression, regardless of what the user inputs (could be any code, just like one would do in the console)

Also, I would like to avoid using libraries (for example to parse and execute the code) for such a simple task. If anyone has some ideas or suggestions it would be greatly appreciated.

Ood
  • 1,445
  • 4
  • 23
  • 43
  • There is no performance issues with `eval`, there is security issues, so why not put the `eval` into the sandboxed `iframe` ? – Keith Jun 05 '23 at 14:35
  • @Keith I haven't tested it myself, but found several articles and questions (such as this one https://stackoverflow.com/questions/86513/why-is-using-the-javascript-eval-function-a-bad-idea) discussing the performance of `eval`. However, even if performance is not an issue there are other problems to consider such as the difficulty for the user to debug their code if it is executed with `eval`. – Ood Jun 05 '23 at 14:44
  • Even in the link you posted it says -> `#3 is less true today than it was in 2008` , also debugging `eval` is fine too, you can even get lines numbers from them if required. But if you don't want to use `eval`, not sure there is anything simple, you'd need to transpile and learn how to AST, not trivial unfortunately. – Keith Jun 05 '23 at 14:52
  • @Keith I tried to benchmark it myself using the code from my question `var x = 0; x++; x;` and the same code with `eval`. There does seem to be a considerable performance hit (60% slower in Firefox and Chrome, 70% slower in Safari) when using `eval`. So it would be great if that were avoidable. – Ood Jun 05 '23 at 15:03
  • What you comparing it too? Was this comparing to injecting `` tag.? – Keith Jun 05 '23 at 15:07
  • @Keith No, just without the `eval` to see what the general performance difference is when using it. But let me create a benchmark for an injected script vs `eval` and I'll edit this when I get the results for `eval` vs an injected script. – Ood Jun 05 '23 at 15:10
  • Well yes, it would be slower. the V8 engine has to compile the code at some point, when you inject with `` tag, it's going to get compiled then. But calling `eval` for code that's not dynamically loaded will of course be magnitudes slower. But that of course won't help you when your making code that gets dynamically modified by your users. – Keith Jun 05 '23 at 15:12
  • @Keith I did benchmark it and the script injection seems to be significantly faster. However, it's difficult to benchmark, as the script that does the injection does not wait for it. So overall I can't say for sure that it's faster than `eval`, only that it has the advantage of being asynchronous. – Ood Jun 05 '23 at 15:39
  • 1
    It won't be any faster, it's the same V8 compiler running in both instances. `eval` is only slower when executing code in the same context for obvious reasons. eg. doing `console.log(1)`, and `eval('console.log(1)')`, the second will be faster, just because of the compiling, but doing `console.time("t1"); let tot = 0; for (let l = 0;l <100000000;l++) tot++; console.timeEnd("t1");` and putting in `eval` there will be hardly any difference. – Keith Jun 05 '23 at 15:55
  • ^^^ oops, `the second will be faster,` , I of course meant the second will be slower.. :) – Keith Jun 05 '23 at 16:10

1 Answers1

1

Instead of eval I use new Function(). That allows me to wrap my code into a function scope and avoid the evaluated code to break/mutate my own code. In your case I see at the moment a few ways to go further:


  • Wrap your eval into a function:

let result = new Function('', 'return eval("var x = 0; x++; x;")');
console.log(result());

So if a user tries to change some outer scopes' variables he will get an error: enter image description here

try{
  let result = new Function('', 'return eval("var x = 0; x++; y++; x;")');
  console.log(result());
}catch(e){
  console.error(e.message);
}

  • Wrap a user's code into a function and return the last expression:

let result = new Function('', 'var x = 0; x++; return x;'); 
console.log(result());

The problem is here that at a glance you need a JS parser here. At least one that splits JS code into statements. Probably could be done by oneself, but I didn't try to write such one. So maybe some 3rd party dependence is needed here at the end.


  • Redeclare window's members:

Also we have some general security problems like access to window. We can override the window's members and itself in the function. This options looks the most promising among the previous ones:

const globals = Object.keys(window);
const overrides = 'let ' + globals.map(name => name + ' = null').join(',') + ';'

const userCode = 'alert("SPAM")';

try{
  let result = new Function('', overrides + `return eval(${userCode})`); 
  console.log(result());
}catch(e){
  console.error(e.message);
}
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17
  • This has the same security concerns as `eval`. – Keith Jun 05 '23 at 14:56
  • @Keith at least outer scopes' variables are protected? – Alexander Nenashev Jun 05 '23 at 14:58
  • @Keith massaging `globalThis` at the moment... – Alexander Nenashev Jun 05 '23 at 15:02
  • Unfortunately just nulling out `globalThis` doesn't fix security issues, be nice if it was that simple.. – Keith Jun 05 '23 at 15:20
  • @Keith ok, i need a pause. the last solution i came to is to `null` all `window`s members inside the function, looks not bad – Alexander Nenashev Jun 05 '23 at 15:27
  • @Keith any code example how to null `globalThis`? – Alexander Nenashev Jun 05 '23 at 15:28
  • Security in the same domain is always going to be tricky, the `iframe` is really the cleanest solution,. eg. try -> `(new ("".constructor.constructor)("alert(49);"))()` in your last example. Even if you catch this, there are chances you might miss something else. Of course `alert` here is not a security concern, but `localeStorage & fetch` in the same Domain would be very dangerous on networked / un-trusted code. There is a good reason why SO does this with it's snippets. – Keith Jun 05 '23 at 15:48
  • @Keith i am implying iframe. should notice that in the answer – Alexander Nenashev Jun 05 '23 at 15:54
  • Yes, `iframe` is all you need for security. Not sure how you was implying iframe though, you go on about access to `window` been the security issue, but that's not the security issue of `eval` and `Function`, its executing code in the same domain that's the security issue. IOW: having access to `window` in an `iframe` is not a security issue, allowing somebody to run `alert` is not security, but an annoyance.. – Keith Jun 05 '23 at 16:08