Algumas aplicações precisam avaliar expressões configuráveis, executar scripts de plugins ou criar contextos JavaScript separados. O módulo VM no Node.js fornece APIs para compilar e executar código em contextos V8 distintos, controlando quais objetos ficam disponíveis.
Apesar do nome e da aparência de sandbox, o módulo node:vm não deve ser tratado como uma barreira de segurança para código hostil. Ele ajuda no isolamento de contexto e na organização de execução, mas não substitui um processo separado, container, máquina virtual ou sandbox projetado para adversários.
Neste guia, você aprenderá a usar runInNewContext(), createContext(), Script, timeouts, módulos, cache de compilação e práticas para reduzir riscos.
Importando o módulo VM
const vm = require('node:vm');Em ES Modules:
import vm from 'node:vm';A documentação oficial do módulo VM alerta que ele não é um mecanismo de segurança. Para isolamento por processo, veja Child Process no Node.js e Worker Threads no Node.js. A OWASP Top 10 apresenta riscos comuns ao executar entrada não confiável.
Executando uma expressão simples
const result = vm.runInNewContext('price * quantity', {
price: 25,
quantity: 4
});
console.log(result); // 100O segundo argumento fornece o contexto global visível ao script. O código não recebe automaticamente as variáveis locais do módulo chamador.
runInNewContext()
Essa função cria um novo contexto, executa o código e retorna o resultado:
const context = {
user: { name: 'Ana' },
output: null
};
vm.runInNewContext(
'output = `Olá, ${user.name}`',
context
);
console.log(context.output);O script pode modificar objetos disponibilizados. Congele ou copie dados quando mutações não forem desejadas.
Objetos compartilhados
const settings = { retries: 3 };
const context = { settings };
vm.runInNewContext('settings.retries = 99', context);
console.log(settings.retries); // 99O contexto separado não clona automaticamente os objetos. Referências compartilhadas continuam apontando para o mesmo valor no processo.
Congelando configuração
const settings = Object.freeze({ retries: 3 });
vm.runInNewContext(
'"use strict"; settings.retries = 99',
{ settings }
);Object.freeze() é superficial. Objetos aninhados precisam de congelamento profundo ou cópia adequada. Mesmo assim, isso não transforma VM em sandbox de segurança.
Criando um contexto reutilizável
const context = vm.createContext({
total: 0,
add(value) {
this.total += value;
}
});
vm.runInContext('add(5)', context);
vm.runInContext('add(7)', context);
console.log(context.total); // 12Reutilizar contexto preserva estado entre execuções. Isso pode ser útil para um REPL ou motor de regras, mas também acumula memória e efeitos inesperados.
Compilando com vm.Script
const script = new vm.Script(
'subtotal * (1 - discount)',
{ filename: 'pricing-rule.js' }
);
const result = script.runInNewContext({
subtotal: 100,
discount: 0.1
});Um Script pode ser compilado uma vez e executado em contextos diferentes. O campo filename melhora stacks e diagnósticos.
Erros de sintaxe
A compilação pode lançar antes da execução:
let script;
try {
script = new vm.Script(source, {
filename: 'custom-rule.js'
});
} catch (error) {
throw new Error(`Regra inválida: ${error.message}`);
}Não retorne stacks internas completas para usuários. Elas podem revelar caminhos e detalhes do servidor.
Timeout de execução
vm.runInNewContext(
'while (true) {}',
{},
{ timeout: 100 }
);O timeout pode interromper JavaScript síncrono dentro da execução. Ele não garante controle completo de recursos nem torna o código hostil seguro.
Timeout não limita memória
Um script pode tentar criar estruturas grandes antes do prazo:
const values = [];
while (true) values.push('x'.repeat(1000000));Para limites fortes de memória, execute em processo separado com restrições do sistema operacional, cgroups ou container.
microtaskMode
Promises e microtarefas podem interagir com o timeout de maneiras específicas. Algumas versões oferecem opções como microtaskMode. Consulte a documentação e crie testes para a versão usada.
Não exponha require()
Este contexto oferece acesso perigoso:
vm.runInNewContext(userCode, {
require,
process,
console
});O script pode carregar módulos, acessar arquivos, rede, ambiente e encerrar o processo. Não passe capacidades amplas para código não confiável.
Princípio da menor capacidade
Disponibilize apenas funções pequenas:
const api = Object.freeze({
round(value, decimals = 2) {
const factor = 10 ** decimals;
return Math.round(value * factor) / factor;
},
max: Math.max,
min: Math.min
});
const result = vm.runInNewContext(source, { api });A função ainda executa no processo principal. Revise argumentos, retorno e possíveis efeitos.
Avaliando fórmulas
Para fórmulas simples, um parser de expressão dedicado costuma ser mais seguro que JavaScript geral. Permitir toda a linguagem inclui loops, funções, protótipos e vários comportamentos difíceis de controlar.
Defina uma gramática com operadores e funções permitidas, limite profundidade e tamanho e rejeite tokens desconhecidos.
Contextos e protótipos
Objetos criados em outro contexto podem ter protótipos diferentes. Isso afeta instanceof:
const array = vm.runInNewContext('[]');
console.log(Array.isArray(array)); // true
console.log(array instanceof Array); // pode ser falseUse verificações apropriadas como Array.isArray() ou util.types.
Serializando resultados
Se o contrato permite apenas dados, converta o resultado para uma estrutura simples:
const safeResult = structuredClone(result);structuredClone() rejeita funções e alguns tipos. Defina limites de profundidade e tamanho antes de aceitar resultados arbitrários.
Console controlado
const messages = [];
const safeConsole = Object.freeze({
log(...args) {
if (messages.length < 100) {
messages.push(args.map(String).join(' '));
}
}
});Limite quantidade e tamanho. Converter objetos complexos pode ser caro ou chamar métodos personalizados.
Módulos com SourceTextModule
APIs como vm.SourceTextModule permitem compilar módulos ECMAScript em contextos personalizados, conforme disponibilidade e estabilidade da versão:
const module = new vm.SourceTextModule(
'export const result = value * 2;',
{ context }
);Linking e avaliação exigem callbacks e políticas claras sobre imports. Não permita resolução arbitrária para sistema de arquivos ou rede.
Controlando imports
Um linker pode permitir apenas módulos aprovados:
async function linker(specifier) {
if (specifier !== 'approved:math') {
throw new Error('Import não permitido');
}
return approvedMathModule;
}Valide também imports dinâmicos, que podem usar outro callback.
Cache de compilação
vm.Script pode gerar ou consumir cached data para reduzir custo de compilação em alguns cenários. O cache precisa corresponder à versão do V8 e não deve ser tratado como código confiável sem validação.
Isolamento com processo separado
Para código de clientes ou plugins não confiáveis, use processo separado:
- usuário do sistema sem privilégios;
- sistema de arquivos somente leitura ou vazio;
- rede bloqueada;
- limites de CPU, memória e tempo;
- protocolo de entrada e saída restrito;
- descarte do processo após a tarefa.
Mesmo um container precisa de configuração segura; ele não é isolamento automático.
Worker Threads não são fronteira de segurança
Workers possuem heap separado, mas compartilham o mesmo processo e permissões. Uma falha nativa ou consumo de recursos pode afetar a aplicação. Use workers para paralelismo confiável, não para código adversarial.
Testando regras
test('calcula desconto', () => {
const script = new vm.Script(
'subtotal * (1 - discount)'
);
const result = script.runInNewContext({
subtotal: 200,
discount: 0.25
});
assert.equal(result, 150);
});Inclua loops infinitos, erros de sintaxe, resultados grandes, mutações e propriedades inesperadas.
Observabilidade
Registre nome da regra, versão, duração, status e timeout. Não grave o código completo quando ele contém dados de cliente. Use hash para correlacionar versões.
Erros comuns
- Chamar VM de sandbox segura: o módulo não foi projetado para adversários.
- Expor require e process: o script ganha acesso ao sistema.
- Confiar apenas em timeout: memória e outros recursos continuam vulneráveis.
- Compartilhar objetos mutáveis: o contexto altera estado do host.
- Usar instanceof entre contextos: protótipos são diferentes.
- Permitir imports arbitrários: arquivos e módulos ficam expostos.
- Reutilizar contexto sem limpeza: estado e memória se acumulam.
Boas práticas
- Use VM para isolamento de contexto, não de segurança.
- Prefira parser dedicado para fórmulas.
- Exponha capacidades mínimas.
- Congele ou clone dados de entrada.
- Defina timeout e limite externo de recursos.
- Valide e copie resultados.
- Controle imports e módulos dinâmicos.
- Use processo ou container para código hostil.
- Teste ataques e consumo de recursos.
- Monitore duração, falhas e timeouts.
Conclusão
O módulo VM no Node.js permite compilar e executar JavaScript em contextos separados, controlar variáveis globais e reutilizar scripts. Ele é útil para regras confiáveis, ferramentas, testes e plugins controlados.
Contexto separado não significa segurança. Código não confiável exige isolamento de processo, permissões mínimas e limites do sistema operacional. Ao usar VM dentro do objetivo correto e reduzir as capacidades expostas, a aplicação ganha flexibilidade sem criar uma falsa sensação de proteção.




