Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Módulo VM no Node.js: Guia Prático

Atualizado em: 10 de agosto de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

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); // 100

O 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); // 99

O 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); // 12

Reutilizar 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 false

Use 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.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita