By Oleksii Rudenko January 11, 2016 7:25 PM
Executing JS Code in a Sandbox with Node's VM Module

The API of Node exposes a module called VM that allows for a more safe execution of arbitrary JS code. The module operates in terms of scripts, sandboxes and contexts.

  • Scripts are objects that represent compiled versions of JS code.
  • Sandboxes are plain JS objects that will be bound to a script before the script is executed. At runtime, the scripts will have access to the sandbox object via the global object.
  • Contexts are sandboxes prepared for usage by the VM module. It’s not possible to pass a sandbox directly to VM. First, it needs to be contextified. The resulting object connects the sandbox and the script’s runtime environment.

How to create a script?

const vm = require('vm');
const script = new vm.Script('throw new Error("Problem");', {
  filename: 'my-index.js', // filename for stack traces
  lineOffset: 1, // line number offset to be used for stack traces
  columnOffset: 1, // column number offset to be used for stack traces
  displayErrors: true,
  timeout: 1000 // ms
});

How to run a script?

There are severalImmediately-Invoked Function Expression options how to run a script and they depend on which context the running script should have:

  • script.runInThisContext(opts) - runs the script in the current scope, i.e. the script will have access to the global variable of the current script but not to the local scope.

  • script.runInNewContext(sandbox, opts) - runs the script in the scope of sandbox, i.e. sandbox.a will be exposed as a inside the script.

  • script.runInContext(context, opts) - runs the script in the context context where the context is the result of vm.createContext on some sandbox object. I.e. runInNewContext calls vm.createContext for you whereas with script.runInContext you can provide a sandbox that was previously contextified by you.

Also it’s possible to omit creation of the script object and use the same methods but on the vm module to create and run scripts in a single step:

 const vm = require('vm');
 vm.runInThisContext(code, opts);
 vm.runInNewContext(code, sandbox, opts);
 vm.runInContext(code, context, opts);

Performance

The code in the sandbox can run slower than normally if you create a new context and use global lookups on that context. For example, the following test code will produce the following results (node v5.4.0, default params):

'use strict';
var code = `
// global script scope
function nop() {
};
var i = 1000000; // global script scope
while (i--) {
  nop(); // access global scope
}
`;

console.time('eval');
eval(code);
console.timeEnd('eval');

var vm = require('vm');
var context = vm.createContext();
var script = new vm.Script(code);

console.time('vm');
script.runInContext(context);
console.timeEnd('vm');

Results:

eval: 11.191ms
vm: 648.014ms

To avoid this, wrap scripts that you run using runInContext or runInNewContext in a immediately-invoked function expression (IIFE).

Safety

Using vm’s module is more safe than relying on eval because scripts run by the vm module do not have access to the outer scope (or do have access only to the global scope as with runInThisContext). Still the scripts run in the same process so for the best security it’s advised to run unsafe scripts in a separate process.

Also by default the scripts do not have access to things like require and console. If you want to allow them, you have to provide it explicitly:

const vm = require('vm');
vm.runInNewContext(`
  var util = require('util');
  console.log(util.isBoolean(true));
`, {
  require: require,
  console: console
});

Thanks for reading.