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 theglobal
variable of the current script but not to the local scope.-
script.runInNewContext(sandbox, opts)
– runs the script in the scope ofsandbox
, i.e.sandbox.a
will be exposed asa
inside the script. -
script.runInContext(context, opts)
– runs the script in thecontext
context where the context is the result ofvm.createContext
on some sandbox object. I.e.runInNewContext
callsvm.createContext
for you whereas withscript.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.