Back to Blog
Node.jsvmSandboxingCode Execution

Node.js vm Module Guide

Master the Node.js vm module for executing JavaScript code in isolated contexts safely.

B
Bootspring Team
Engineering
August 11, 2019
7 min read

The vm module provides APIs for compiling and running code within V8 virtual machine contexts. Here's how to use it.

Basic Script Execution#

1import vm from 'node:vm'; 2 3// Run code in current context 4const code = 'x + y'; 5const result = vm.runInThisContext(code); 6// Error: x is not defined (no access to local variables) 7 8// Run with context 9const context = { x: 10, y: 20 }; 10vm.createContext(context); 11const result = vm.runInContext('x + y', context); 12console.log(result); // 30 13 14// Run in new context 15const result = vm.runInNewContext('x + y', { x: 10, y: 20 }); 16console.log(result); // 30

Creating Contexts#

1import vm from 'node:vm'; 2 3// Create a reusable context 4const sandbox = { 5 console: console, 6 Math: Math, 7 result: null, 8}; 9 10vm.createContext(sandbox); 11 12// Run multiple scripts in same context 13vm.runInContext('result = Math.sqrt(16)', sandbox); 14console.log(sandbox.result); // 4 15 16vm.runInContext('result = result * 2', sandbox); 17console.log(sandbox.result); // 8 18 19// Check if object is a context 20console.log(vm.isContext(sandbox)); // true 21console.log(vm.isContext({})); // false

Script Compilation#

1import vm from 'node:vm'; 2 3// Compile script for reuse 4const script = new vm.Script('x + y'); 5 6// Run in different contexts 7const context1 = vm.createContext({ x: 1, y: 2 }); 8const context2 = vm.createContext({ x: 10, y: 20 }); 9 10console.log(script.runInContext(context1)); // 3 11console.log(script.runInContext(context2)); // 30 12 13// Script with options 14const timedScript = new vm.Script('while(true) {}', { 15 filename: 'infinite-loop.js', 16 lineOffset: 0, 17 columnOffset: 0, 18}); 19 20// Run with timeout 21try { 22 timedScript.runInNewContext({}, { timeout: 100 }); 23} catch (err) { 24 console.log('Script timed out'); 25}

Execution Options#

1import vm from 'node:vm'; 2 3const code = ` 4 function calculate() { 5 return a * b; 6 } 7 calculate(); 8`; 9 10const options = { 11 // Timeout in milliseconds 12 timeout: 1000, 13 14 // Display filename in stack traces 15 filename: 'user-code.js', 16 17 // Adjust line numbers in stack traces 18 lineOffset: 10, 19 columnOffset: 5, 20 21 // Break on first statement (debugging) 22 breakOnSigint: true, 23}; 24 25const context = vm.createContext({ a: 5, b: 10 }); 26const result = vm.runInContext(code, context, options); 27console.log(result); // 50

Sandboxed Environment#

1import vm from 'node:vm'; 2 3// Create minimal sandbox 4function createSandbox() { 5 const sandbox = { 6 // Safe built-ins 7 console: { 8 log: (...args) => console.log('[Sandbox]', ...args), 9 }, 10 Math: Math, 11 Date: Date, 12 JSON: JSON, 13 parseInt: parseInt, 14 parseFloat: parseFloat, 15 16 // Custom APIs 17 result: undefined, 18 }; 19 20 return vm.createContext(sandbox); 21} 22 23const sandbox = createSandbox(); 24 25// Code can't access Node.js APIs 26vm.runInContext(` 27 console.log('Hello from sandbox'); 28 result = Math.PI * 2; 29`, sandbox); 30 31console.log(sandbox.result); // 6.283... 32 33// Dangerous code is blocked 34try { 35 vm.runInContext(` 36 require('fs').readFileSync('/etc/passwd'); 37 `, sandbox); 38} catch (err) { 39 console.log('Blocked:', err.message); 40 // require is not defined 41}

Module Support#

1import vm from 'node:vm'; 2 3// Create module 4const context = vm.createContext({ 5 console: console, 6}); 7 8// Synthetic module 9const syntheticModule = new vm.SyntheticModule( 10 ['default', 'named'], 11 function() { 12 this.setExport('default', 'default export'); 13 this.setExport('named', 'named export'); 14 }, 15 { context } 16); 17 18// Link and evaluate 19await syntheticModule.link(() => {}); 20await syntheticModule.evaluate(); 21 22console.log(syntheticModule.namespace); 23// { default: 'default export', named: 'named export' }

Safe Evaluation#

1import vm from 'node:vm'; 2 3// Safe eval function 4function safeEval(code, context = {}, options = {}) { 5 const sandbox = { 6 ...context, 7 // Prevent access to dangerous globals 8 process: undefined, 9 require: undefined, 10 module: undefined, 11 exports: undefined, 12 __dirname: undefined, 13 __filename: undefined, 14 }; 15 16 vm.createContext(sandbox); 17 18 const defaultOptions = { 19 timeout: 1000, 20 filename: 'eval', 21 }; 22 23 return vm.runInContext(code, sandbox, { 24 ...defaultOptions, 25 ...options, 26 }); 27} 28 29// Usage 30const result = safeEval('2 + 2'); 31console.log(result); // 4 32 33const result2 = safeEval('items.filter(x => x > 5)', { 34 items: [1, 3, 5, 7, 9], 35}); 36console.log(result2); // [7, 9]

Expression Evaluation#

1import vm from 'node:vm'; 2 3// Evaluate user expressions 4class ExpressionEvaluator { 5 constructor() { 6 this.context = vm.createContext({ 7 Math: Math, 8 Date: Date, 9 }); 10 this.variables = {}; 11 } 12 13 setVariable(name, value) { 14 this.variables[name] = value; 15 this.context[name] = value; 16 } 17 18 evaluate(expression) { 19 try { 20 return vm.runInContext(expression, this.context, { 21 timeout: 100, 22 }); 23 } catch (err) { 24 throw new Error(`Evaluation error: ${err.message}`); 25 } 26 } 27} 28 29const evaluator = new ExpressionEvaluator(); 30evaluator.setVariable('price', 100); 31evaluator.setVariable('quantity', 5); 32 33console.log(evaluator.evaluate('price * quantity')); // 500 34console.log(evaluator.evaluate('Math.round(price * 1.1)')); // 110

Template Engine#

1import vm from 'node:vm'; 2 3// Simple template engine using vm 4function renderTemplate(template, data) { 5 const sandbox = { 6 ...data, 7 __output: '', 8 __append: function(str) { 9 this.__output += str; 10 }, 11 }; 12 13 vm.createContext(sandbox); 14 15 // Convert template to JavaScript 16 let code = '__append(`'; 17 code += template 18 .replace(/`/g, '\\`') 19 .replace(/\${/g, '`); __append(') 20 .replace(/}/g, '); __append(`'); 21 code += '`);'; 22 23 vm.runInContext(code, sandbox, { timeout: 1000 }); 24 25 return sandbox.__output; 26} 27 28// Usage 29const template = 'Hello, ${name}! You have ${count} messages.'; 30const result = renderTemplate(template, { name: 'John', count: 5 }); 31console.log(result); // 'Hello, John! You have 5 messages.'

Code Analysis#

1import vm from 'node:vm'; 2 3// Analyze code without executing 4function analyzeCode(code) { 5 try { 6 new vm.Script(code, { filename: 'analysis.js' }); 7 return { valid: true, error: null }; 8 } catch (err) { 9 return { 10 valid: false, 11 error: { 12 message: err.message, 13 line: err.lineNumber, 14 column: err.columnNumber, 15 }, 16 }; 17 } 18} 19 20console.log(analyzeCode('const x = 1;')); 21// { valid: true, error: null } 22 23console.log(analyzeCode('const x = ;')); 24// { valid: false, error: { message: 'Unexpected token ;', ... } }

Worker-like Isolation#

1import vm from 'node:vm'; 2 3// Create isolated execution environment 4class IsolatedRunner { 5 constructor() { 6 this.context = vm.createContext({ 7 console: { 8 log: (...args) => this.output.push(['log', args]), 9 error: (...args) => this.output.push(['error', args]), 10 }, 11 setTimeout: (fn, ms) => { 12 if (ms > 5000) throw new Error('Timeout too long'); 13 return setTimeout(fn, ms); 14 }, 15 }); 16 this.output = []; 17 } 18 19 async run(code, timeout = 5000) { 20 this.output = []; 21 22 const script = new vm.Script(` 23 (async () => { 24 ${code} 25 })(); 26 `); 27 28 try { 29 await script.runInContext(this.context, { 30 timeout, 31 breakOnSigint: true, 32 }); 33 } catch (err) { 34 this.output.push(['error', [err.message]]); 35 } 36 37 return this.output; 38 } 39} 40 41// Usage 42const runner = new IsolatedRunner(); 43const output = await runner.run(` 44 console.log('Starting...'); 45 const result = 1 + 2; 46 console.log('Result:', result); 47`); 48 49console.log(output); 50// [['log', ['Starting...']], ['log', ['Result:', 3]]]

Security Considerations#

1import vm from 'node:vm'; 2 3// IMPORTANT: vm is NOT a security sandbox! 4// Malicious code can escape the context 5 6// Example of context escape (DO NOT rely on vm for security) 7const malicious = ` 8 const ForeignFunction = this.constructor.constructor; 9 const process = ForeignFunction('return process')(); 10 process.exit(1); 11`; 12 13// For true isolation, use: 14// 1. Worker threads with limited permissions 15// 2. Child processes 16// 3. Containers/VMs 17// 4. Third-party sandboxing libraries (vm2, isolated-vm) 18 19// Safe pattern: validate and whitelist operations 20function safeExecute(code) { 21 // Only allow specific patterns 22 const allowedPattern = /^[\d\s+\-*/().]+$/; 23 if (!allowedPattern.test(code)) { 24 throw new Error('Invalid expression'); 25 } 26 27 return vm.runInNewContext(code, {}); 28} 29 30console.log(safeExecute('2 + 2')); // 4 31// safeExecute('require("fs")'); // Error: Invalid expression

Best Practices#

Use Cases: ✓ Simple expression evaluation ✓ Template rendering ✓ Configuration evaluation ✓ Code analysis/validation Context Setup: ✓ Minimal sandbox ✓ Whitelist allowed APIs ✓ Set timeouts ✓ Validate input Security: ✓ vm is NOT a security sandbox ✓ Validate/sanitize input ✓ Use for trusted code only ✓ Consider alternatives for untrusted code Avoid: ✗ Running untrusted code ✗ No timeout limits ✗ Exposing Node.js APIs ✗ Assuming isolation

Conclusion#

The Node.js vm module enables code execution in isolated V8 contexts, useful for template engines, expression evaluation, and code analysis. However, it is NOT a security sandbox - malicious code can escape. Use it for semi-trusted code with input validation and timeouts. For truly untrusted code, consider worker threads, child processes, or purpose-built sandboxing solutions like isolated-vm.

Share this article

Help spread the word about Bootspring