14

I want to use the vm module as a safe way to run external code. It works pretty well, but there is one issue left:

var UNKNOWN_CODE = "while(true){}";

var vm = require("vm");

var obj = {};
var ctx = vm.createContext(obj);

var script = vm.createScript(UNKNOWN_CODE);

script.runInNewContext(ctx);

console.log("finished"); //never executed

Is there any way to cancel the execution (e.g. if it lasts for more than 5s)?

Thanks in advance!

muffel
  • 7,004
  • 8
  • 57
  • 98

5 Answers5

26

You need to run it in a separate process, for example:

master.js:

var cluster = require('cluster');

cluster.setupMaster({
  exec : "runner.js",
  args : process.argv.slice(2),
  silent : false
});
//This will be fired when the forked process becomes online
cluster.on( "online", function(worker) {
    var timer = 0;

    worker.on( "message", function(msg) {
        clearTimeout(timer); //The worker responded in under 5 seconds, clear the timeout
        console.log(msg);
        worker.destroy(); //Don't leave him hanging 

    });
    timer = setTimeout( function() {
        worker.destroy(); //Give it 5 seconds to run, then abort it
        console.log("worker timed out");
    }, 5000);

    worker.send( 'while(true){}' ); //Send the code to run for the worker
});
cluster.fork();

runner.js:

//The runner.js is ran in a separate process and just listens for the message which contains code to be executed
process.on('message', function( UNKNOWN_CODE ) {

    var vm = require("vm");

    var obj = {};
    var ctx = vm.createContext(obj);

    var script = vm.createScript(UNKNOWN_CODE);

    script.runInNewContext(ctx);

    process.send( "finished" ); //Send the finished message to the parent process
});

To run this example, place those files in the same folder and dir to it and run

node master.js

You should see "worker timed out" message after 5 seconds. If you change it to 'while(false){}' the worker will execute the code immediately and you should see "finished" instead.

Cluster docs

Esailija
  • 138,174
  • 23
  • 272
  • 326
  • thank you for your answer. Unfortunately this won't work for me as I need to pass buffers to and from the context and afaik buffers can't be transfered using the limited Node IPC. Any other ideas? – muffel Jul 31 '12 at 09:53
  • 1
    @muffel sorry, another process is a **must** here. There is no way for the process itself to execute the code AND execute some other code that would keep track of it. That's physically impossible because node.js process is single threaded. As for buffers, I don't know. Maybe converting them to arrays of numbers representing bytes? – Esailija Jul 31 '12 at 10:15
  • @Esailija does that mean that the vm2 node package (https://www.npmjs.com/package/vm2) cannot work as advertised? – Jodes Dec 11 '16 at 00:30
  • Please note that `worker.destroy()` is obsolete. It is now `worker.kill()`. But only `worker.process.kill()` guarantees 'kill'. See https://nodejs.org/api/cluster.html#cluster_worker_kill_signal_sigterm It is easy to see if you keep forking, for instance with `setInterval`. You will notice, that the number of workers slowly grows if `worker.destroy()` (or `worker.kill()`, they are aliases) is used. With `worker.process.kill()` it is always one process. – Do-do-new Jul 03 '19 at 14:39
6

You can embed "script breaker" into UNKNOWN_CODE. Something like:

;setTimeout(function() { throw new Error("Execution time limit reached!") }, 2000);

So, the whole thing would look like this:

var UNKNOWN_CODE = "while(true){}";

var scriptBreaker = ';setTimeout(function() { throw new Error("Execution time limit reached!") }, 2000);';

var vm = require("vm");

var obj = {};
var ctx = vm.createContext(obj);

var script = vm.createScript(scriptBreaker + UNKNOWN_CODE);

try {
  script.runInNewContext(ctx);
  console.log("Finished");
}
catch (err) { 
  console.log("Timeout!"); 
  // Handle Timeout Error...
}

UPDATE:

After more tests I've come to conclusion that reliable approach would be to use process as pointed Esailija. However, I'm doing it a bit differently.

In main app I have a code like this:

var cp = require('child_process');

function runUnsafeScript(script, callback) {
 var worker = cp.fork('./script-runner', [script]);

 worker.on('message', function(data) {
  worker.kill();
  callback(false, data);
 });

 worker.on('exit', function (code, signal) {
  callback(new Error(code), false);
 });

 worker.on('error', function (err) {
  callback(err, false);
 });

 setTimeout(function killOnTimeOut() {
  worker.kill();
  callback(new Error("Timeout"), false);
 }, 5000);
}

In script-runner.js it looks like following:

var vm = require("vm");
var script = vm.createScript( process.argv[2] );
var obj = { sendResult:function (result) { process.send(result); process.exit(0); } };
var context = vm.createContext(obj);

script.runInNewContext(context);

process.on('uncaughtException', function(err) {
 process.exit(1);
});

This approach allowed to achieve following goals:

  • run script in a limited context
  • avoid problems with dead loops and exceptions
  • run many (limited by hardware) unsafe scripts simultaneously so they don't interrupt each other
  • pass script execution results to the main app for further processing
Pavel Reva
  • 91
  • 1
  • 5
  • This is 1. unnecessary since vm now supports a native timeout (see accepted answer), 2. insecure, as you could `clearTimeout()` (see http://stackoverflow.com/questions/8860188/is-there-a-way-to-clear-all-time-outs), 3. has an overall "hacky" taste. – Lukas Mar 18 '14 at 22:41
  • 1
    I think this may be the only proper way. How would you handle the case where the untrusted script starts a while loop in a callback from setTimeout or any asynchronous function it is allowed to use? The only other thing you can do is use something like https://github.com/dallonf/async-eval but requires some bookkeeping. – NG. Mar 07 '15 at 03:54
4

Yes, this is now possible because I added timeout parameter support to the Node vm module. You can simply pass in a millisecond timeout value to runInNewContext() and it will throw an exception if the code does not finish executing in the specified amount of time.

Note, this does not imply any kind of security model for running untrusted code. This simply allows you to timeout code which you do trust or otherwise secure.

var vm = require("vm");

try {
    vm.runInNewContext("while(true) {}", {}, "loop", 1000);
} catch (e) {
    // Exception thrown after 1000ms
}

console.log("finished"); // Will now be executed

Exactly what you would expect:

$ time ./node test.js
finished

real    0m1.069s
user    0m1.047s
sys     0m0.017s
Andrew Paprocki
  • 273
  • 2
  • 5
  • 1
    Which version of Node introduces it? Is it possible to have timeout for `runInContext` as well? – Adrian Jul 01 '13 at 18:44
  • 4
    @Andrew, this does not work in Node v0.10.18. (or any other versions I tried). Copy and pasted verbatim and the server just hangs. – Anders Sep 12 '13 at 21:06
  • 1
    This is actually added in Node `v0.11` and beyond. I added an answer below describing how to use it (the syntax is a little different). You use `vm.runInNewContext('while(true) {}', {}, {timeout: 1000});` – Ryan Endacott May 12 '14 at 03:33
  • 2
    This returns immediately when the internal script issues an async command and then it can do whatever it wants in the callback regardless of the timeout set. https://github.com/dallonf/async-eval is an option, but I am wondering if there is any other trick to detect evil aside from killing the process. – NG. Mar 07 '15 at 03:58
  • Is there a way to terminate `vm` from outside without setting timeout in advance? – Mitar Nov 02 '15 at 22:44
  • This can be easily escaped as described here: https://github.com/nodejs/node/issues/3020. – idmean Feb 26 '16 at 20:53
3

You might wanna check Threads a Gogo. Unfortunately, it hasn't been updated to 0.8.x yet.

However, as mentioned by @Esailija, there is no way to run external code safely unless it's in another process.

var Threads = require('threads_a_gogo');

var t = Threads.create();
t.eval("while(true) { console.log('.'); }");

setTimeout(function() {
  t.destroy();
  console.log('finished');
}, 1000);
Laurent Perrin
  • 14,671
  • 5
  • 50
  • 49
0

In newer versions of Node.js (v0.12 and onward), you will be able to pass a timeout option to vm.runInNewContext.

This isn't in any stable node release yet, but if you wish to use the latest unstable release (v0.11.13), you can pass a timeout parameter in like this:

vm.runInNewContext('while(1) {}', {}, {timeout: '1000'});

Now, after 1000 milliseconds the script will throw a timeout error. You can catch it like this:

try {
    vm.runInNewContext('while(1) {}', {}, {timeout: '1000'});
}
catch(e) {
    console.log(e); // Script execution timed out.
}
Ryan Endacott
  • 8,772
  • 4
  • 27
  • 39